From 126f929c3961b3f897ba9fa35890af27d0421961 Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Tue, 27 Jun 2017 23:10:43 +0200 Subject: [PATCH 001/114] i18n: Use instance name in email notifications instead of Mastodon (pl) (#3976) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcin Mikołajczak --- app/views/user_mailer/password_change.pl.html.erb | 2 +- app/views/user_mailer/password_change.pl.text.erb | 2 +- app/views/user_mailer/reset_password_instructions.pl.html.erb | 3 ++- app/views/user_mailer/reset_password_instructions.pl.text.erb | 3 ++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/views/user_mailer/password_change.pl.html.erb b/app/views/user_mailer/password_change.pl.html.erb index 46423483a0d..a7cb15a05e3 100644 --- a/app/views/user_mailer/password_change.pl.html.erb +++ b/app/views/user_mailer/password_change.pl.html.erb @@ -1,3 +1,3 @@

Witaj, <%= @resource.email %>!

-

Informujemy, że ostatnio zmieniono Twoje hasło Mastodona.

+

Informujemy, że ostatnio zmieniono Twoje hasło na <%= @instance %>.

diff --git a/app/views/user_mailer/password_change.pl.text.erb b/app/views/user_mailer/password_change.pl.text.erb index 85d5e117522..bd2efee0f35 100644 --- a/app/views/user_mailer/password_change.pl.text.erb +++ b/app/views/user_mailer/password_change.pl.text.erb @@ -1,3 +1,3 @@ Witaj, <%= @resource.email %>! -Informujemy, że ostatnio zmieniono Twoje hasło Mastodona. +Informujemy, że ostatnio zmieniono Twoje hasło na <%= @instance %>. diff --git a/app/views/user_mailer/reset_password_instructions.pl.html.erb b/app/views/user_mailer/reset_password_instructions.pl.html.erb index f4d67c724fa..2a9913a1d52 100644 --- a/app/views/user_mailer/reset_password_instructions.pl.html.erb +++ b/app/views/user_mailer/reset_password_instructions.pl.html.erb @@ -1,6 +1,7 @@

Witaj, <%= @resource.email %>!

-

Ktoś próbował zmienić Twoje hasło na Mastodonie. Możesz zrobić to klikając w poniższy link.

+

Ktoś próbował zmienić Twoje hasło na <%= @instance %>. Możesz zrobić to klikając w +poniższy link.

<%= link_to 'Zmień moje hasło', edit_password_url(@resource, reset_password_token: @token) %>

diff --git a/app/views/user_mailer/reset_password_instructions.pl.text.erb b/app/views/user_mailer/reset_password_instructions.pl.text.erb index 78d1cab0b2e..2b34afc489d 100644 --- a/app/views/user_mailer/reset_password_instructions.pl.text.erb +++ b/app/views/user_mailer/reset_password_instructions.pl.text.erb @@ -1,6 +1,7 @@ Witaj, <%= @resource.email %>! -Ktoś próbował zmienić Twoje hasło na Mastodonie. Możesz zrobić to klikając w poniższy link. +Ktoś próbował zmienić Twoje hasło na <%= @instance %>. Możesz zrobić to klikając w +poniższy link. <%= edit_password_url(@resource, reset_password_token: @token) %> From 2a9805b987537d0327ab4623049e2a4d3180f8e4 Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Tue, 27 Jun 2017 23:14:02 +0200 Subject: [PATCH 002/114] i18n: Minor fix in devise.pl.yml (#3978) --- config/locales/devise.pl.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/devise.pl.yml b/config/locales/devise.pl.yml index 792e0d81e40..1c692f7a87a 100644 --- a/config/locales/devise.pl.yml +++ b/config/locales/devise.pl.yml @@ -8,10 +8,10 @@ pl: failure: already_authenticated: Jesteś już zalogowany/zalogowana. inactive: Twoje konto nie zostało jeszcze aktywowane. - invalid: Błędne %{authentication_keys} lub hasło. + invalid: Nieprawidłowy %{authentication_keys} lub hasło. last_attempt: Masz jeszcze jedną próbę; Twoje konto zostanie zablokowane jeśli się nie powiedzie. locked: Twoje konto zostało zablokowane. - not_found_in_database: Błędne %{authentication_keys} lub hasło. + not_found_in_database: Nieprawidłowy %{authentication_keys} lub hasło. timeout: Twoja sesja wygasła. Zaloguj się ponownie aby kontynuować.. unauthenticated: Zapisz się lub zaloguj aby kontynuować. unconfirmed: Zweryfikuj adres e-mail aby kontynuować. From fb421a1f46d956e62d27d0227e6853c709f2c88e Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Wed, 28 Jun 2017 14:07:53 +0200 Subject: [PATCH 003/114] i18n: added email to activerecord.pl.yml (#3981) --- config/locales/activerecord.pl.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/locales/activerecord.pl.yml b/config/locales/activerecord.pl.yml index 627f612bb5d..f82e1b87585 100644 --- a/config/locales/activerecord.pl.yml +++ b/config/locales/activerecord.pl.yml @@ -1,5 +1,8 @@ pl: activerecord: + attributes: + user: + email: adres e-mail errors: models: account: From 7d8e3721aea71315b0ef8e66cdc2ede0fe6ffc2a Mon Sep 17 00:00:00 2001 From: "Akihiko Odaki (@fn_aki@pawoo.net)" Date: Wed, 28 Jun 2017 21:50:23 +0900 Subject: [PATCH 004/114] Overwrite old statuses with reblogs in PrecomputeFeedService (#3984) --- app/services/precompute_feed_service.rb | 2 +- spec/models/feed_spec.rb | 6 +++--- spec/rails_helper.rb | 6 ++++++ spec/services/precompute_feed_service_spec.rb | 8 ++++---- spec/workers/scheduler/feed_cleanup_scheduler_spec.rb | 7 +++++-- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb index 83765bb0570..4c24567c8bd 100644 --- a/app/services/precompute_feed_service.rb +++ b/app/services/precompute_feed_service.rb @@ -14,7 +14,7 @@ class PrecomputeFeedService < BaseService def populate_feed redis.pipelined do - statuses.each do |status| + statuses.reverse_each do |status| process_status(status) end diff --git a/spec/models/feed_spec.rb b/spec/models/feed_spec.rb index 15033e9ebbf..1cdb3a7839f 100644 --- a/spec/models/feed_spec.rb +++ b/spec/models/feed_spec.rb @@ -8,13 +8,13 @@ RSpec.describe Feed, type: :model do Fabricate(:status, account: account, id: 2) Fabricate(:status, account: account, id: 3) Fabricate(:status, account: account, id: 10) - redis = double(zrevrangebyscore: [['val2', 2.0], ['val1', 1.0], ['val3', 3.0], ['deleted', 4.0]], exists: false) - allow(Redis).to receive(:current).and_return(redis) + Redis.current.zadd(FeedManager.instance.key(:home, account.id), + [[4, 'deleted'], [3, 'val3'], [2, 'val2'], [1, 'val1']]) feed = Feed.new(:home, account) results = feed.get(3) - expect(results.map(&:id)).to eq [2, 1, 3] + expect(results.map(&:id)).to eq [3, 2] expect(results.first.attributes.keys).to eq %w(id updated_at) end end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index cfc9eec9ea0..9a4c8fd3c9f 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -13,6 +13,7 @@ Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } ActiveRecord::Migration.maintain_test_schema! WebMock.disable_net_connect! +Redis.current = Redis::Namespace.new("mastodon_test#{ENV['TEST_ENV_NUMBER']}", redis: Redis.current) Sidekiq::Testing.inline! Sidekiq::Logging.logger = nil @@ -43,6 +44,11 @@ RSpec.configure do |config| https = ENV['LOCAL_HTTPS'] == 'true' Capybara.app_host = "http#{https ? 's' : ''}://#{ENV.fetch('LOCAL_DOMAIN')}" end + + config.after :each do + keys = Redis.current.keys + Redis.current.del(keys) if keys.any? + end end RSpec::Sidekiq.configure do |config| diff --git a/spec/services/precompute_feed_service_spec.rb b/spec/services/precompute_feed_service_spec.rb index 9f56b025603..72235a9664f 100644 --- a/spec/services/precompute_feed_service_spec.rb +++ b/spec/services/precompute_feed_service_spec.rb @@ -11,12 +11,12 @@ RSpec.describe PrecomputeFeedService do account = Fabricate(:account) followed_account = Fabricate(:account) Fabricate(:follow, account: account, target_account: followed_account) - status = Fabricate(:status, account: followed_account) - - expected_redis_args = FeedManager.instance.key(:home, account.id), status.id, status.id - expect_any_instance_of(Redis).to receive(:zadd).with(*expected_redis_args) + reblog = Fabricate(:status, account: followed_account) + status = Fabricate(:status, account: account, reblog: reblog) subject.call(account) + + expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq status.id end end end diff --git a/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb b/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb index 4c709a2c9f9..b8487b03ffe 100644 --- a/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb +++ b/spec/workers/scheduler/feed_cleanup_scheduler_spec.rb @@ -7,10 +7,13 @@ describe Scheduler::FeedCleanupScheduler do let!(:inactive_user) { Fabricate(:user, current_sign_in_at: 22.days.ago) } it 'clears feeds of inactives' do - expect_any_instance_of(Redis).to receive(:del).with(feed_key_for(inactive_user)) - expect_any_instance_of(Redis).not_to receive(:del).with(feed_key_for(active_user)) + Redis.current.zadd(feed_key_for(inactive_user), 1, 1) + Redis.current.zadd(feed_key_for(active_user), 1, 1) subject.perform + + expect(Redis.current.zcard(feed_key_for(inactive_user))).to eq 0 + expect(Redis.current.zcard(feed_key_for(active_user))).to eq 1 end def feed_key_for(user) From e4fee6c138b7fdab3820e9cf6406b1d5195269e8 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Wed, 28 Jun 2017 23:45:21 +0900 Subject: [PATCH 005/114] Add Japanese translations (#3985) ref #3929, #3935, #3949, #3981 --- config/locales/activerecord.ja.yml | 3 ++ config/locales/ja.yml | 45 +++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/config/locales/activerecord.ja.yml b/config/locales/activerecord.ja.yml index 6e6b48496b3..975912f0f27 100644 --- a/config/locales/activerecord.ja.yml +++ b/config/locales/activerecord.ja.yml @@ -1,5 +1,8 @@ ja: activerecord: + attributes: + user: + email: メールアドレス errors: models: account: diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 80169339da0..a60811e50f7 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -193,6 +193,10 @@ ja: title: PubSubHubbub topic: トピック title: 管理 + admin_mailer: + new_report: + body: "%{reporter} が %{target} を通報しました" + subject: "%{instance} の新しい通報 (#%{id})" application_mailer: settings: 'メール設定の変更: %{link}' signature: Mastodon %{instance} インスタンスからの通知 @@ -320,6 +324,43 @@ ja: missing_resource: リダイレクト先が見つかりませんでした proceed: フォローする prompt: 'フォローしようとしています:' + sessions: + activity: 最後のアクティビティ + browser: ブラウザ + browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + firefox: Firefox + generic: 不明なブラウザ + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: 現在のセッション + description: "%{browser} on %{platform}" + explanation: あなたのMastodonアカウントに現在ログインしているウェブブラウザの一覧です。 + ip: IP + platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac + other: 不明なプラットフォーム + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + title: セッション settings: authorized_apps: 認証済みアプリ back: 戻る @@ -354,13 +395,15 @@ ja: description_html: "二段階認証を有効にするとログイン時、電話でコードを受け取る必要があります。" disable: 無効 enable: 有効 + enabled: 二段階認証は有効になっています enabled_success: 二段階認証が有効になりました generate_recovery_codes: リカバリーコードを生成 instructions_html: "Google Authenticatorか、もしくはほかのTOTPアプリでこのQRコードをスキャンしてください。これ以降、ログインするときはそのアプリで生成されるコードが必要になります。" lost_recovery_codes: リカバリーコードを使用すると携帯電話を紛失した場合でもアカウントにアクセスできるようになります。 リカバリーコードを紛失した場合もここで再生成することができますが、古いリカバリーコードは無効になります。 manual_instructions: 'QRコードがスキャンできず、手動での登録を希望の場合はこのシークレットコードを利用してください。:' + recovery_codes: リカバリーコード recovery_codes_regenerated: リカバリーコードが再生成されました。 - recovery_instructions_html: 携帯電話を紛失した場合、以下の内どれかのリカバリーコードを使用してアカウントへアクセスすることができます。 リカバリーコードは印刷して安全に保管してください。 + recovery_instructions_html: 携帯電話を紛失した場合、以下の内どれかのリカバリーコードを使用してアカウントへアクセスすることができます。リカバリーコードは大切に保全してください。たとえば印刷してほかの重要な書類と一緒に保管することができます。 setup: 初期設定 wrong_code: コードが間違っています。サーバー上の時間とデバイス上の時間が一致していることを確認してください。 users: From 71bc75e6ac00050ff98609b3cbeb38a0db157e1f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 28 Jun 2017 17:43:48 +0200 Subject: [PATCH 006/114] Do not fail to create access token if superapp was never created (#3986) --- app/models/session_activation.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/models/session_activation.rb b/app/models/session_activation.rb index 02a918e8ac5..887e3e3bd4f 100644 --- a/app/models/session_activation.rb +++ b/app/models/session_activation.rb @@ -69,9 +69,7 @@ class SessionActivation < ApplicationRecord def assign_access_token superapp = Doorkeeper::Application.find_by(superapp: true) - return if superapp.nil? - - self.access_token = Doorkeeper::AccessToken.create!(application_id: superapp.id, + self.access_token = Doorkeeper::AccessToken.create!(application_id: superapp&.id, resource_owner_id: user_id, scopes: 'read write follow', expires_in: Doorkeeper.configuration.access_token_expires_in, From b6a19e7b89fb4b32f35e810580a5aea7ff87addd Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 28 Jun 2017 17:44:17 +0200 Subject: [PATCH 007/114] Bump version to 1.4.7 --- lib/mastodon/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 44f3e4390b3..3c92ce41792 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 6 + 7 end def pre From 60b2b56d380c7cd3dc0ba54f4650cfdba568e38e Mon Sep 17 00:00:00 2001 From: "Akihiko Odaki (@fn_aki@pawoo.net)" Date: Thu, 29 Jun 2017 08:17:26 +0900 Subject: [PATCH 008/114] Reduce number of commands in FeedManager#trim (#3989) --- app/lib/feed_manager.rb | 4 +--- spec/lib/feed_manager_spec.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 90a1441f293..c507f263653 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -38,9 +38,7 @@ class FeedManager end def trim(type, account_id) - return unless redis.zcard(key(type, account_id)) > FeedManager::MAX_ITEMS - last = redis.zrevrange(key(type, account_id), FeedManager::MAX_ITEMS - 1, FeedManager::MAX_ITEMS - 1) - redis.zremrangebyscore(key(type, account_id), '-inf', "(#{last.last}") + redis.zremrangebyrank(key(type, account_id), '0', (-(FeedManager::MAX_ITEMS + 1)).to_s) end def push_update_required?(timeline_type, account_id) diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb index bf474c35430..4bdc96866d1 100644 --- a/spec/lib/feed_manager_spec.rb +++ b/spec/lib/feed_manager_spec.rb @@ -131,4 +131,17 @@ RSpec.describe FeedManager do end end end + + describe '#push' do + it 'trims timelines if they will have more than FeedManager::MAX_ITEMS' do + account = Fabricate(:account) + status = Fabricate(:status) + members = FeedManager::MAX_ITEMS.times.map { |count| [count, count] } + Redis.current.zadd("feed:type:#{account.id}", members) + + FeedManager.instance.push('type', account, status) + + expect(Redis.current.zcard("feed:type:#{account.id}")).to eq FeedManager::MAX_ITEMS + end + end end From f79c10162e51689f6759dfa39251c8ba8e7e11e8 Mon Sep 17 00:00:00 2001 From: "Akihiko Odaki (@fn_aki@pawoo.net)" Date: Thu, 29 Jun 2017 08:25:31 +0900 Subject: [PATCH 009/114] Use multiple pairs for zadd in PrecomputeFeedService (#3990) --- app/services/precompute_feed_service.rb | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb index 4c24567c8bd..e2f6ff0cb84 100644 --- a/app/services/precompute_feed_service.rb +++ b/app/services/precompute_feed_service.rb @@ -13,21 +13,16 @@ class PrecomputeFeedService < BaseService attr_reader :account def populate_feed - redis.pipelined do - statuses.reverse_each do |status| - process_status(status) - end + pairs = statuses.reverse_each.map(&method(:process_status)) + redis.pipelined do + redis.zadd(account_home_key, pairs) redis.del("account:#{@account.id}:regeneration") end end def process_status(status) - add_status_to_feed(status) unless status_filtered?(status) - end - - def add_status_to_feed(status) - redis.zadd(account_home_key, status.id, status.reblog? ? status.reblog_of_id : status.id) + [status.id, status.reblog? ? status.reblog_of_id : status.id] unless status_filtered?(status) end def status_filtered?(status) From 0a53ca444a4bd4b28eddf2064133c38a65d41f2c Mon Sep 17 00:00:00 2001 From: "Akihiko Odaki (@fn_aki@pawoo.net)" Date: Thu, 29 Jun 2017 08:43:10 +0900 Subject: [PATCH 010/114] Cover Admin::AccountsController more (#3327) --- .../admin/accounts_controller_spec.rb | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/spec/controllers/admin/accounts_controller_spec.rb b/spec/controllers/admin/accounts_controller_spec.rb index 305260475ab..8be27d86685 100644 --- a/spec/controllers/admin/accounts_controller_spec.rb +++ b/spec/controllers/admin/accounts_controller_spec.rb @@ -3,11 +3,64 @@ require 'rails_helper' RSpec.describe Admin::AccountsController, type: :controller do render_views + let(:user) { Fabricate(:user, admin: true) } + before do - sign_in Fabricate(:user, admin: true), scope: :user + sign_in user, scope: :user end describe 'GET #index' do + around do |example| + default_per_page = Account.default_per_page + Account.paginates_per 1 + example.run + Account.paginates_per default_per_page + end + + it 'filters with parameters' do + new = AccountFilter.method(:new) + + expect(AccountFilter).to receive(:new) do |params| + h = params.to_h + + expect(h[:local]).to eq '1' + expect(h[:remote]).to eq '1' + expect(h[:by_domain]).to eq 'domain' + expect(h[:silenced]).to eq '1' + expect(h[:recent]).to eq '1' + expect(h[:suspended]).to eq '1' + expect(h[:username]).to eq 'username' + expect(h[:display_name]).to eq 'display name' + expect(h[:email]).to eq 'local-part@domain' + expect(h[:ip]).to eq '0.0.0.42' + + new.call({}) + end + + get :index, params: { + local: '1', + remote: '1', + by_domain: 'domain', + silenced: '1', + recent: '1', + suspended: '1', + username: 'username', + display_name: 'display name', + email: 'local-part@domain', + ip: '0.0.0.42' + } + end + + it 'paginates accounts' do + Fabricate(:account) + + get :index, params: { page: 2 } + + accounts = assigns(:accounts) + expect(accounts.count).to eq 1 + expect(accounts.klass).to be Account + end + it 'returns http success' do get :index expect(response).to have_http_status(:success) From ead14f5bf0ae43524f055320f373a6e2ce947476 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Thu, 29 Jun 2017 20:03:03 +0900 Subject: [PATCH 011/114] Upgrade jsdom to version 11.0.0 (#3994) --- package.json | 2 +- spec/javascript/setup.js | 18 ++++++------------ yarn.lock | 20 +++++++++++++------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 60f9af1e92c..feb59dc9027 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "eslint": "^3.19.0", "eslint-plugin-jsx-a11y": "^4.0.0", "eslint-plugin-react": "^6.10.3", - "jsdom": "^10.1.0", + "jsdom": "^11.0.0", "mocha": "^3.4.1", "react-intl-translations-manager": "^5.0.0", "react-test-renderer": "^15.6.1", diff --git a/spec/javascript/setup.js b/spec/javascript/setup.js index 7d4b2866efe..c9c8aed077e 100644 --- a/spec/javascript/setup.js +++ b/spec/javascript/setup.js @@ -1,19 +1,13 @@ -import { jsdom } from 'jsdom/lib/old-api'; +import { JSDOM } from 'jsdom'; import chai from 'chai'; import chaiEnzyme from 'chai-enzyme'; chai.use(chaiEnzyme()); -var exposedProperties = ['window', 'navigator', 'document']; - -global.document = jsdom(''); -global.window = document.defaultView; -Object.keys(document.defaultView).forEach((property) => { +const { window } = new JSDOM('', { + userAgent: 'node.js', +}); +Object.keys(window).forEach(property => { if (typeof global[property] === 'undefined') { - exposedProperties.push(property); - global[property] = document.defaultView[property]; + global[property] = window[property]; } }); - -global.navigator = { - userAgent: 'node.js', -}; diff --git a/yarn.lock b/yarn.lock index b8af49d6295..4b5e3ae1748 100644 --- a/yarn.lock +++ b/yarn.lock @@ -121,6 +121,10 @@ react-split-pane "^0.1.63" redux "^3.6.0" +"@types/node@^6.0.46": + version "6.0.78" + resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.78.tgz#5d4a3f579c1524e01ee21bf474e6fba09198f470" + abab@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d" @@ -3911,9 +3915,9 @@ jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" -jsdom@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-10.1.0.tgz#7765e00fd5c3567f34985a1c86ff466a61dacc6a" +jsdom@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.0.0.tgz#1ee507cb2c0b16c875002476b1a8557d951353e5" dependencies: abab "^1.0.3" acorn "^4.0.4" @@ -3925,7 +3929,7 @@ jsdom@^10.1.0: escodegen "^1.6.1" html-encoding-sniffer "^1.0.1" nwmatcher ">= 1.3.9 < 2.0.0" - parse5 "^1.5.1" + parse5 "^3.0.2" pn "^1.0.0" request "^2.79.0" request-promise-native "^1.0.3" @@ -4919,9 +4923,11 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" -parse5@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" +parse5@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.2.tgz#05eff57f0ef4577fb144a79f8b9a967a6cc44510" + dependencies: + "@types/node" "^6.0.46" parseurl@~1.3.1: version "1.3.1" From b342c81c17cc8cf4af2ac3b1c57f4f250e0fefc1 Mon Sep 17 00:00:00 2001 From: abcang Date: Thu, 29 Jun 2017 20:04:07 +0900 Subject: [PATCH 012/114] rescue HTTP::ConnectionError (#3992) --- app/services/fetch_atom_service.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/services/fetch_atom_service.rb b/app/services/fetch_atom_service.rb index 8f42db0aaa4..d430b22e93f 100644 --- a/app/services/fetch_atom_service.rb +++ b/app/services/fetch_atom_service.rb @@ -20,6 +20,10 @@ class FetchAtomService < BaseService process_html(fetch(url)) rescue OpenSSL::SSL::SSLError => e Rails.logger.debug "SSL error: #{e}" + nil + rescue HTTP::ConnectionError => e + Rails.logger.debug "HTTP ConnectionError: #{e}" + nil end private From 049cea30b0f3900484d45b0d88a0f073ad0c6cc6 Mon Sep 17 00:00:00 2001 From: Naoki Kosaka Date: Fri, 30 Jun 2017 12:37:17 +0900 Subject: [PATCH 013/114] Fix media-gallery, overflow is hidden. (#4008) --- app/javascript/styles/components.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 88431fc69af..4df4b0685e6 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -3433,6 +3433,7 @@ button.icon-button.active i.fa-retweet { &, img { width: 100%; + overflow: hidden; } } From a27879c0cf89d99fb79e2ffbe7ecfdf72733a1c4 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 30 Jun 2017 12:37:41 +0900 Subject: [PATCH 014/114] Replace state to /web when root path (#4009) --- app/javascript/mastodon/main.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js index 02e0f56f957..aca64c07512 100644 --- a/app/javascript/mastodon/main.js +++ b/app/javascript/mastodon/main.js @@ -20,6 +20,14 @@ function main() { require.context('../images/', true); + if (window.history && history.replaceState) { + const { pathname, search, hash } = window.location; + const path = pathname + search + hash; + if (!(/^\/web[$/]/).test(path)) { + history.replaceState(null, document.title, `/web${path}`); + } + } + onDomContentLoaded(() => { const mountNode = document.getElementById('mastodon'); const props = JSON.parse(mountNode.getAttribute('data-props')); From 1273fbf86ea3bd906e687a33e1f62c99f100ecca Mon Sep 17 00:00:00 2001 From: abcang Date: Fri, 30 Jun 2017 20:38:36 +0900 Subject: [PATCH 015/114] Rescue Addressable::URI::InvalidURIError at Remotable (#4017) --- app/models/concerns/remotable.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index 4a412ee3d51..b0077ce96eb 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -26,8 +26,9 @@ module Remotable send("#{attachment_name}_file_name=", filename) self[attribute_name] = url if has_attribute?(attribute_name) - rescue HTTP::TimeoutError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError => e + rescue HTTP::TimeoutError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError => e Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}" + nil end end end From 7362469d8956d5f972283aadd4157631aa66b085 Mon Sep 17 00:00:00 2001 From: "Akihiko Odaki (@fn_aki@pawoo.net)" Date: Fri, 30 Jun 2017 20:39:42 +0900 Subject: [PATCH 016/114] Do not raise an error if PrecomputeFeed could not find any status (#4015) --- app/services/precompute_feed_service.rb | 2 +- spec/services/precompute_feed_service_spec.rb | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb index e2f6ff0cb84..a32ba1dae4f 100644 --- a/app/services/precompute_feed_service.rb +++ b/app/services/precompute_feed_service.rb @@ -16,7 +16,7 @@ class PrecomputeFeedService < BaseService pairs = statuses.reverse_each.map(&method(:process_status)) redis.pipelined do - redis.zadd(account_home_key, pairs) + redis.zadd(account_home_key, pairs) if pairs.any? redis.del("account:#{@account.id}:regeneration") end end diff --git a/spec/services/precompute_feed_service_spec.rb b/spec/services/precompute_feed_service_spec.rb index 72235a9664f..e2294469c03 100644 --- a/spec/services/precompute_feed_service_spec.rb +++ b/spec/services/precompute_feed_service_spec.rb @@ -18,5 +18,10 @@ RSpec.describe PrecomputeFeedService do expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq status.id end + + it 'does not raise an error even if it could not find any status' do + account = Fabricate(:account) + subject.call(account) + end end end From 0e09048537fee9906ab582bc0704e1a3395c04ed Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 30 Jun 2017 20:40:00 +0900 Subject: [PATCH 017/114] Fix broken style in media gallery (regression from #3963) (#4014) --- app/javascript/styles/components.scss | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 4df4b0685e6..28cb9ec65a3 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -3421,19 +3421,16 @@ button.icon-button.active i.fa-retweet { } .media-gallery__item-thumbnail { - background-position: center; - background-repeat: no-repeat; - background-size: cover; cursor: zoom-in; - display: flex; - align-items: center; + display: block; text-decoration: none; height: 100%; &, img { width: 100%; - overflow: hidden; + height: 100%; + object-fit: cover; } } From 5c7a4f0b324129777fa81e0bd7128a3711543c0e Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 30 Jun 2017 20:40:14 +0900 Subject: [PATCH 018/114] Remove babel-cli (#4011) --- package.json | 1 - yarn.lock | 55 ++-------------------------------------------------- 2 files changed, 2 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index feb59dc9027..7fa80a0c2fc 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "array-includes": "^3.0.3", "autoprefixer": "^7.1.0", "axios": "^0.16.2", - "babel-cli": "^6.24.1", "babel-core": "^6.25.0", "babel-loader": "^7.1.0", "babel-plugin-lodash": "^3.2.11", diff --git a/yarn.lock b/yarn.lock index 4b5e3ae1748..adabca08d19 100644 --- a/yarn.lock +++ b/yarn.lock @@ -460,27 +460,6 @@ axios@^0.16.2: follow-redirects "^1.2.3" is-buffer "^1.1.5" -babel-cli@^6.24.1: - version "6.24.1" - resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.24.1.tgz#207cd705bba61489b2ea41b5312341cf6aca2283" - dependencies: - babel-core "^6.24.1" - babel-polyfill "^6.23.0" - babel-register "^6.24.1" - babel-runtime "^6.22.0" - commander "^2.8.1" - convert-source-map "^1.1.0" - fs-readdir-recursive "^1.0.0" - glob "^7.0.0" - lodash "^4.2.0" - output-file-sync "^1.1.0" - path-is-absolute "^1.0.0" - slash "^1.0.0" - source-map "^0.5.0" - v8flags "^2.0.10" - optionalDependencies: - chokidar "^1.6.1" - babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" @@ -1095,14 +1074,6 @@ babel-plugin-transform-strict-mode@^6.24.1: babel-runtime "^6.22.0" babel-types "^6.24.1" -babel-polyfill@^6.23.0: - version "6.23.0" - resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.23.0.tgz#8364ca62df8eafb830499f699177466c3b03499d" - dependencies: - babel-runtime "^6.22.0" - core-js "^2.4.0" - regenerator-runtime "^0.10.0" - babel-preset-env@1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.4.0.tgz#c8e02a3bcc7792f23cded68e0355b9d4c28f0f7a" @@ -1657,7 +1628,7 @@ cheerio@^0.22.0: lodash.reject "^4.4.0" lodash.some "^4.4.0" -chokidar@^1.4.3, chokidar@^1.6.0, chokidar@^1.6.1: +chokidar@^1.4.3, chokidar@^1.6.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" dependencies: @@ -3055,10 +3026,6 @@ fs-promise@^0.3.1: dependencies: any-promise "~0.1.0" -fs-readdir-recursive@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -3286,7 +3253,7 @@ gonzales-pe@^4.0.3: dependencies: minimist "1.1.x" -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9: +graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -4864,14 +4831,6 @@ osenv@0, osenv@^0.1.4: os-homedir "^1.0.0" os-tmpdir "^1.0.0" -output-file-sync@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" - dependencies: - graceful-fs "^4.1.4" - mkdirp "^0.5.1" - object-assign "^4.1.0" - p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" @@ -7154,10 +7113,6 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" -user-home@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" - user-home@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/user-home/-/user-home-2.0.0.tgz#9c70bfd8169bc1dcbf48604e0f04b8b49cde9e9f" @@ -7190,12 +7145,6 @@ uws@^0.14.5: version "0.14.5" resolved "https://registry.yarnpkg.com/uws/-/uws-0.14.5.tgz#67aaf33c46b2a587a5f6666d00f7691328f149dc" -v8flags@^2.0.10: - version "2.1.1" - resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" - dependencies: - user-home "^1.1.1" - validate-npm-package-license@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" From 3a7106f05ae0bc39b084916076ec6ee3733c32c1 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 30 Jun 2017 20:40:43 +0900 Subject: [PATCH 019/114] Fix that AdminMailer does not send (#4012) --- app/mailers/admin_mailer.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index fc19a6d40bb..fd9223533af 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class AdminMailer < ApplicationMailer + helper StreamEntriesHelper + def new_report(recipient, report) @report = report @me = recipient From 59ddf81a4533044452bd7e6a25dd4469cc660ffb Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 30 Jun 2017 07:42:04 -0400 Subject: [PATCH 020/114] Version bumps for gems (#4002) * Update aws-sdk to version 2.10.4 * Update bootsnap to version 1.1.1 * Update capistrano to version 3.8.2 * Update capybara to version 2.14.4 * Update cld3 to version 3.1.3 * Update http_accept_language to version 2.1.1 * Update sidekiq to version 5.0.3 * Update rspec-sidekiq to version 3.0.3 * Update sidekiq-scheduler to version 2.1.7 * Update oj to version 3.2.0 * Update openssl to version 2.0.4 * Update pg to version 0.21.0 * Update twitter-text to version 1.14.6 * Update unicode-display_width to version 1.3.0 * Update scss_lint to version 0.54.0 * Update hamlit to version 2.8.4 * Update erubi to version 1.6.1 * Update httplog to version 0.99.4 * Update aws-sdk to version 2.10.6 --- Gemfile.lock | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c19f31e0102..dac6169d574 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -41,7 +41,7 @@ GEM tzinfo (~> 1.1) addressable (2.5.1) public_suffix (~> 2.0, >= 2.0.2) - airbrussh (1.2.0) + airbrussh (1.3.0) sshkit (>= 1.6.1, != 1.7.0) annotate (2.7.2) activerecord (>= 3.2, < 6.0) @@ -52,13 +52,13 @@ GEM encryptor (~> 3.0.0) av (0.9.0) cocaine (~> 0.5.3) - aws-sdk (2.9.37) - aws-sdk-resources (= 2.9.37) - aws-sdk-core (2.9.37) + aws-sdk (2.10.6) + aws-sdk-resources (= 2.10.6) + aws-sdk-core (2.10.6) aws-sigv4 (~> 1.0) jmespath (~> 1.0) - aws-sdk-resources (2.9.37) - aws-sdk-core (= 2.9.37) + aws-sdk-resources (2.10.6) + aws-sdk-core (= 2.10.6) aws-sigv4 (1.0.0) bcrypt (3.1.11) better_errors (2.1.1) @@ -67,7 +67,7 @@ GEM rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - bootsnap (1.0.0) + bootsnap (1.1.1) msgpack (~> 1.0) brakeman (3.6.2) browser (2.4.0) @@ -78,7 +78,7 @@ GEM bundler-audit (0.5.0) bundler (~> 1.2) thor (~> 0.18) - capistrano (3.8.1) + capistrano (3.8.2) airbrussh (>= 1.0.0) i18n rake (>= 10.0.0) @@ -94,7 +94,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (2.14.2) + capybara (2.14.4) addressable mime-types (>= 1.16) nokogiri (>= 1.3.3) @@ -102,7 +102,7 @@ GEM rack-test (>= 0.5.4) xpath (~> 2.0) chunky_png (1.3.8) - cld3 (3.1.2) + cld3 (3.1.3) ffi (>= 1.1.0, < 1.10.0) climate_control (0.2.0) cocaine (0.5.8) @@ -142,9 +142,9 @@ GEM thread thread_safe encryptor (3.0.0) - erubi (1.6.0) + erubi (1.6.1) erubis (2.7.0) - et-orbi (1.0.4) + et-orbi (1.0.5) tzinfo execjs (2.7.0) fabrication (2.16.1) @@ -161,7 +161,7 @@ GEM addressable (~> 2.4) http (~> 2.0) nokogiri (~> 1.6) - hamlit (2.8.1) + hamlit (2.8.4) temple (>= 0.8.0) thor tilt @@ -182,9 +182,9 @@ GEM http-cookie (1.0.3) domain_name (~> 0.5) http-form_data (1.0.3) - http_accept_language (2.1.0) + http_accept_language (2.1.1) http_parser.rb (0.6.0) - httplog (0.99.3) + httplog (0.99.4) colorize rack i18n (0.8.4) @@ -249,8 +249,8 @@ GEM mini_portile2 (~> 2.2.0) nokogumbo (1.4.13) nokogiri - oj (3.1.0) - openssl (2.0.3) + oj (3.2.0) + openssl (2.0.4) orm_adapter (0.5.0) ostatus2 (2.0.1) addressable (~> 2.4) @@ -272,7 +272,7 @@ GEM parallel parser (2.4.0.0) ast (~> 2.2) - pg (0.20.0) + pg (0.21.0) pghero (1.7.0) activerecord pkg-config (1.2.3) @@ -374,7 +374,7 @@ GEM rspec-expectations (~> 3.6.0) rspec-mocks (~> 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) sidekiq (>= 2.4.0) rspec-support (3.6.0) @@ -395,10 +395,10 @@ GEM nokogiri (>= 1.4.4) nokogumbo (~> 1.4.1) sass (3.4.24) - scss_lint (0.53.0) + scss_lint (0.54.0) rake (>= 0.9, < 13) sass (~> 3.4.20) - sidekiq (5.0.2) + sidekiq (5.0.3) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) rack-protection (>= 1.5.0) @@ -406,7 +406,7 @@ GEM sidekiq-bulk (0.1.1) activesupport sidekiq - sidekiq-scheduler (2.1.5) + sidekiq-scheduler (2.1.7) redis (~> 3) rufus-scheduler (~> 3.2) sidekiq (>= 3) @@ -443,7 +443,7 @@ GEM thread (0.2.2) thread_safe (0.3.6) tilt (2.0.7) - twitter-text (1.14.5) + twitter-text (1.14.6) unf (~> 0.1.0) tzinfo (1.2.3) thread_safe (~> 0.1) @@ -454,7 +454,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.4) - unicode-display_width (1.2.1) + unicode-display_width (1.3.0) uniform_notifier (1.10.0) warden (1.2.7) rack (>= 1.0) From 968354923ec5063946d561e41194870ce37ec43a Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Fri, 30 Jun 2017 04:43:26 -0700 Subject: [PATCH 021/114] Fix webpack-dev-server on Windows (#4000) * Fix webpack-dev-server on Windows * Serve webpack from 0.0.0.0, access at 127.0.0.1 --- bin/webpack-dev-server | 2 +- config/webpacker.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/webpack-dev-server b/bin/webpack-dev-server index a867f2c0137..0beec31753b 100755 --- a/bin/webpack-dev-server +++ b/bin/webpack-dev-server @@ -23,7 +23,7 @@ end begin dev_server = YAML.load_file(CONFIG_FILE)["development"]["dev_server"] - DEV_SERVER_HOST = "http#{"s" if args('--https') || dev_server["https"]}://#{args('--host') || dev_server["host"]}:#{args('--port') || dev_server["port"]}" + DEV_SERVER_HOST = "http#{"s" if args('--https') || dev_server["https"]}://#{dev_server["host"]}:#{args('--port') || dev_server["port"]}" rescue Errno::ENOENT, NoMethodError puts "Webpack dev_server configuration not found in #{CONFIG_FILE}." diff --git a/config/webpacker.yml b/config/webpacker.yml index c1cd6e93b27..aa429a1ddac 100644 --- a/config/webpacker.yml +++ b/config/webpacker.yml @@ -19,7 +19,7 @@ development: <<: *default dev_server: - host: 0.0.0.0 + host: 127.0.0.1 port: 8080 https: false From 6dd5eac7fc81f2283525f954db812d937153bcfc Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 30 Jun 2017 07:43:34 -0400 Subject: [PATCH 022/114] Add controller spec for manifests controller (#4003) --- spec/controllers/manifests_controller_spec.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 spec/controllers/manifests_controller_spec.rb diff --git a/spec/controllers/manifests_controller_spec.rb b/spec/controllers/manifests_controller_spec.rb new file mode 100644 index 00000000000..6f188fa352c --- /dev/null +++ b/spec/controllers/manifests_controller_spec.rb @@ -0,0 +1,19 @@ +require 'rails_helper' + +describe ManifestsController do + render_views + + describe 'GET #show' do + before do + get :show, format: :json + end + + it 'assigns @instance_presenter' do + expect(assigns(:instance_presenter)).to be_kind_of InstancePresenter + end + + it 'returns http success' do + expect(response).to have_http_status(:success) + end + end +end From a978b88997169782ac35f416bf88d6afd60edd1e Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Fri, 30 Jun 2017 08:29:22 -0700 Subject: [PATCH 023/114] Faster emojify() algorithm, avoid regex replace (#4019) * Faster emojify() algorithm, avoid regex replace * add semicolon --- app/javascript/mastodon/emoji.js | 43 +++++++++++++++---- spec/javascript/components/emojify.test.js | 49 ++++++++++++++++++++++ 2 files changed, 83 insertions(+), 9 deletions(-) create mode 100644 spec/javascript/components/emojify.test.js diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js index 01d01fb7208..d0df71ea378 100644 --- a/app/javascript/mastodon/emoji.js +++ b/app/javascript/mastodon/emoji.js @@ -19,16 +19,41 @@ const unicodeToImage = str => { }); }; -const shortnameToImage = str => str.replace(emojione.regShortNames, shortname => { - if (typeof shortname === 'undefined' || shortname === '' || !(shortname in emojione.emojioneList)) { - return shortname; +const shortnameToImage = str => { + // This walks through the string from end to start, ignoring any tags (

,
, etc.) + // and replacing valid shortnames like :smile: and :wink: that _aren't_ within + // tags with an version. + // The goal is to be the same as an emojione.regShortNames replacement, but faster. + // The reason we go backwards is because then we can replace substrings as we go. + let i = str.length; + let insideTag = false; + let insideShortname = false; + let shortnameEndIndex = -1; + while (i--) { + const char = str.charAt(i); + if (insideShortname && char === ':') { + const shortname = str.substring(i, shortnameEndIndex + 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 = `${alt}`; + str = str.substring(0, i) + replacement + str.substring(shortnameEndIndex + 1); + } 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; + shortnameEndIndex = i; + } } - - const unicode = emojione.emojioneList[shortname].unicode[emojione.emojioneList[shortname].unicode.length - 1]; - const alt = emojione.convert(unicode.toUpperCase()); - - return `${alt}`; -}); + return str; +}; export default function emojify(text) { return toImage(text); diff --git a/spec/javascript/components/emojify.test.js b/spec/javascript/components/emojify.test.js new file mode 100644 index 00000000000..7a496623e55 --- /dev/null +++ b/spec/javascript/components/emojify.test.js @@ -0,0 +1,49 @@ +import { expect } from 'chai'; +import emojify from '../../../app/javascript/mastodon/emoji'; + +describe('emojify', () => { + it('does a basic emojify', () => { + expect(emojify(':smile:')).to.equal( + '😄'); + }); + + it('does a double emojify', () => { + expect(emojify(':smile: and :wink:')).to.equal( + '😄 and 😉'); + }); + + it('works with random colons', () => { + expect(emojify(':smile: : :wink:')).to.equal( + '😄 : 😉'); + expect(emojify(':smile::::wink:')).to.equal( + '😄::😉'); + expect(emojify(':smile:::::wink:')).to.equal( + '😄:::😉'); + }); + + it('works with tags', () => { + expect(emojify('

:smile:

')).to.equal( + '

😄

'); + expect(emojify('

:smile:

and

:wink:

')).to.equal( + '

😄

and

😉

'); + }); + + it('ignores unknown shortcodes', () => { + expect(emojify(':foobarbazfake:')).to.equal(':foobarbazfake:'); + }); + + it('ignores shortcodes inside of tags', () => { + expect(emojify('

')).to.equal('

'); + }); + + it('works with unclosed tags', () => { + expect(emojify('hello>')).to.equal('hello>'); + expect(emojify(' { + expect(emojify('smile:')).to.equal('smile:'); + expect(emojify(':smile')).to.equal(':smile'); + }); + +}); From bf50e3e5aefc88f7a6d9ab4aafe5beab4360292b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 1 Jul 2017 14:50:10 +0200 Subject: [PATCH 024/114] Fix height issue in report modal --- app/javascript/styles/components.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 28cb9ec65a3..a87aa5d79bf 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -3278,6 +3278,7 @@ button.icon-button.active i.fa-retweet { .report-modal__statuses { min-height: 20vh; + max-height: 40vh; overflow-y: auto; overflow-x: hidden; } From d1d94216d1534243180e99245232f769542639d3 Mon Sep 17 00:00:00 2001 From: unarist Date: Sun, 2 Jul 2017 04:18:38 +0900 Subject: [PATCH 025/114] Update Japanese translation (Credentials -> Security) (#4025) --- config/locales/ja.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/ja.yml b/config/locales/ja.yml index a60811e50f7..52752e7d185 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -204,7 +204,7 @@ ja: applications: invalid_url: URLが無効です auth: - change_password: ログイン情報 + change_password: セキュリティ delete_account: アカウントの削除 delete_account_html: アカウントを削除したい場合、こちらから手続きが行えます。削除前には確認画面があります。 didnt_get_confirmation: 確認メールを受信できませんか? From 60da49f8562b970e514ba4403cac1944229a5768 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Sun, 2 Jul 2017 15:55:50 +0200 Subject: [PATCH 026/114] fix(components/columns_area): Increase delta for swipe detection (#4037) --- app/javascript/mastodon/features/ui/components/columns_area.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 3c3e9425d36..01167b6e5d2 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -53,7 +53,7 @@ export default class ColumnsArea extends ImmutablePureComponent { if (singleColumn) { return ( - + {children} ); From 133b892e0d95c27de28c22e3d0efe284c75f7700 Mon Sep 17 00:00:00 2001 From: Damien Erambert Date: Sun, 2 Jul 2017 09:36:35 -0700 Subject: [PATCH 027/114] Update French locales (#4034) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add missing locales for French translation * accent "Media" in the front-end locales * images => médias * Change 'rapport' to 'signalement' in French locales to be more coherent * fix typo * remove duplicate EN locale * translate missing locales * update missing locale * fix typo * unify with "utilisateur⋅ice⋅s" * address PR comments --- app/javascript/mastodon/locales/fr.json | 16 ++-- config/locales/fr.yml | 122 ++++++++++++++++++++++-- 2 files changed, 123 insertions(+), 15 deletions(-) diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 1a69235c892..fd2b3044423 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -7,7 +7,7 @@ "account.followers": "Abonné⋅e⋅s", "account.follows": "Abonnements", "account.follows_you": "Vous suit", - "account.media": "Media", + "account.media": "Média", "account.mention": "Mentionner", "account.mute": "Masquer", "account.posts": "Statuts", @@ -61,11 +61,11 @@ "emoji_button.travel": "Lieux et voyages", "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.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.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.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.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 utilisateur⋅ice⋅s d’autres instances pour remplir le fil public.", "follow_request.authorize": "Autoriser", "follow_request.reject": "Rejeter", "getting_started.appsshort": "Applications", @@ -108,11 +108,11 @@ "notifications.column_settings.sound": "Émettre un son", "onboarding.done": "Effectué", "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_four.home": "L’Accueil affiche les posts de tou⋅te⋅s les utilisateurs⋅trices que vous suivez", + "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 utilisateur⋅ice⋅s que vous suivez", "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.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_six.admin": "L’administrateur⋅trice de votre instance est {admin}", "onboarding.page_six.almost_done": "Nous y sommes presque…", @@ -123,7 +123,7 @@ "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_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.skip": "Passer", "privacy.change": "Ajuster la confidentialité du message", diff --git a/config/locales/fr.yml b/config/locales/fr.yml index dfe5ff990ca..5a3e0c55297 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -27,8 +27,9 @@ fr: status_count_after: posts status_count_before: Ayant publié terms: Conditions d’utilisation - user_count_after: utilisateurs⋅trices + user_count_after: utilisateur⋅ice⋅s user_count_before: Abrite + version: Version accounts: follow: Suivre followers: Abonné⋅es @@ -38,10 +39,23 @@ fr: people_who_follow: Personnes qui suivent %{name} posts: Statuts remote_follow: Suivre à distance + reserved_username: Ce nom d'utilisateur⋅ice est réservé unfollow: Ne plus suivre + activitypub: + activity: + announce: + name: "%{account_name} a partagé une activité." + create: + name: "%{account_name} a créé une note." + outbox: + name: "Boîte d'envoi de %{account_name}" + summary: Liste d'activités de %{account_name} admin: accounts: - are_you_sure: Êtes-vous certain ? + are_you_sure: Êtes-vous certain⋅e ? + confirm: Confirmer + confirmed: Confirmé + disable_two_factor_authentication: Désactiver l'authentification à deux facteurs display_name: Nom affiché domain: Domaine edit: Éditer @@ -49,6 +63,7 @@ fr: feed_url: URL du flux followers: Abonné⋅es follows: Abonnements + ip: Adresse IP location: all: Tous local: Local @@ -71,14 +86,24 @@ fr: profile_url: URL du profil public: Public push_subscription_expires: Expiration de l'abonnement PuSH + redownload: Rafraîchir les avatars + reset: Réinitialiser reset_password: Réinitialiser le mot de passe + resubscribe: Se réabonner salmon_url: URL Salmon + search: Rechercher + show: + created_reports: Signalements créés par ce compte + report: signalement + targeted_reports: Signalements créés visant ce compte silence: Rendre muet statuses: Statuts + subscribe: S'abonner title: Comptes undo_silenced: Annuler le silence undo_suspension: Annuler la suspension - username: Nom d'utilisateur + unsubscribe: Se désabonner + username: Nom d'utilisateur⋅ice web: Web domain_blocks: add_new: Ajouter @@ -110,14 +135,24 @@ fr: undo: Annuler title: Blocage de domaines undo: Annuler + instances: + account_count: Comptes connus + domain_name: Domaine + title: Instances connues reports: + action_taken_by: Intervention de + are_you_sure: Êtes vous certain⋅e? comment: label: Commentaire none: Aucun delete: Supprimer id: ID mark_as_resolved: Marquer comme résolu + nsfw: + 'false': Ré-afficher les médias + 'true': Masquer les médias report: 'Signalement #%{id}' + report_contents: Contenu reported_account: Compte signalé reported_by: Signalé par resolved: Résolus @@ -132,7 +167,7 @@ fr: contact_information: email: Entrez une adresse courriel publique label: Informations de contact - username: Entrez un nom d'utilisateur + username: Entrez un nom d'utilisateur⋅ice registrations: closed_message: desc_html: Affiché sur la page d'accueil lorsque les inscriptions sont fermées
Vous pouvez utiliser des balises HTML @@ -158,6 +193,10 @@ fr: title: PubSubHubbub topic: Sujet title: Administration + admin_mailer: + new_report: + body: "%{reporter} a signalé %{target}" + subject: Nouveau signalement sur %{instance} (#%{id}) application_mailer: settings: 'Changer les préférences courriel : %{link}' signature: Notifications de Mastodon depuis %{instance} @@ -166,6 +205,8 @@ fr: invalid_url: L'URL fournie est invalide auth: change_password: Changer de mot de passe + delete_account: Supprimer le compte + delete_account_html: Si vous désirez supprimer votre compte, vous pouvez cliquer ici. Il vous sera demandé de confirmer cette action. didnt_get_confirmation: Vous n’avez pas reçu les consignes de confirmation ? forgot_password: Mot de passe oublié ? login: Se connecter @@ -199,18 +240,41 @@ fr: x_minutes: "%{count}min" x_months: "%{count}mois" x_seconds: "%{count}s" + deletes: + bad_password_msg: Bien essayé ! Mot de passe incorrect + confirm_password: Entrez votre mot de passe pour vérifier votre identité + description_html: Cela va supprimer votre compte et le désactiver de manière permanente et irréversible. Votre nom d'utilisateur⋅ice restera réservé afin d'éviter la confusion + proceed: Supprimer compte + success_msg: Votre compte a été supprimé avec succès + warning_html: Seule la suppression du contenu depuis cette instance est garantie. Le contenu qui a été partagé est susceptible de laisser des traces. Les serveurs hors-lignes ainsi que ceux n'étant plus abonnés à vos publications ne mettront pas leur base de données à jour. + warning_title: Disponibilité du contenu disséminé errors: + '403': Vous n'avez pas accès à cette page. '404': La page que vous recherchez n'existe pas. '410': La page que vous recherchez n'existe plus. '422': content: Vérification de sécurité échouée. Bloquez-vous les cookies ? title: Vérification de sécurité échouée + '429': Trop de requêtes émises dans un délai donné. + noscript: Pour utiliser Mastodon, veuillez activer JavaScript exports: blocks: Vous bloquez csv: CSV follows: Vous suivez mutes: Vous faites taire storage: Médias stockés + followers: + domain: Domaine + explanation_html: Si vous voulez être sûr⋅e que vos status restent privés, vous devez savoir qui vous suit. Vos status privés seront diffusés à toutes les instances des utilisateur⋅ice⋅s qui vous suivent. Vous voudrez peut-être les passer en revue et les supprimer si vous n'êtes pas sûr⋅e que votre vie privée sera respectée par l'administration ou le logiciel de ces instances. + followers_count: Nombre d'abonné⋅es + lock_link: Rendez votre compte privé + purge: Retirer de la liste d'abonné⋅es + success: + one: Suppression des abonné⋅es venant d'un domaine en cours... + other: Suppression des abonné⋅es venant de %{count} domaines en cours... + true_privacy_html: Soyez conscient⋅es qu'une vraie confidentialité ne peut être atteinte que par un chiffrement de bout-en-bout. + unlocked_warning_html: N'importe qui peut vous suivre et voir vos status privés. %{lock_link} afin de pouvoir vérifier et rejeter des abonné⋅es. + unlocked_warning_title: Votre compte n'est pas privé generic: changes_saved_msg: Les modifications ont été enregistrées avec succès ! powered_by: propulsé par %{link} @@ -222,9 +286,9 @@ fr: preface: Vous pouvez importer certaines données comme les personnes que vous suivez ou bloquez sur votre compte sur cette instance à partir de fichiers créés sur une autre instance. success: Vos données ont été importées avec succès et seront traitées en temps et en heure types: - blocking: Liste d'utilisateurs⋅trices bloqué⋅es - following: Liste d'utilisateurs⋅trices suivi⋅es - muting: Liste d'utilisateurs⋅trices que vous faites taire + blocking: Liste d'utilisateur⋅ice⋅s bloqué⋅es + following: Liste d'utilisateur⋅ice⋅s suivi⋅es + muting: Liste d'utilisateur⋅ice⋅s que vous faites taire upload: Importer landing_strip_html: %{name} utilise %{link_to_root_path}. Vous pouvez le/la suivre et interagir si vous possédez un compte quelque part dans le "fediverse". landing_strip_signup_html: Si ce n'est pas le cas, vous pouvez en créer un ici. @@ -265,11 +329,50 @@ fr: missing_resource: L'URL de redirection n'a pas pu être trouvée proceed: Continuez pour suivre prompt: 'Vous allez suivre :' + sessions: + activity: Dernière activité + browser: Navigateur + browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + firefox: Firefox + generic: Navigateur inconnu + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: Session courante + description: "%{browser} sur %{platform}" + explanation: Ceci est la liste des navigateurs actuellement connectés à votre compte Mastodon. + ip: Adresse IP + platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac + other: système inconnu + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + title: Sessions settings: authorized_apps: Applications autorisées back: Retour vers Mastodon + delete: Suppression de compte edit_profile: Modifier le profil export: Export de données + followers: Abonné⋅es autorisé⋅es import: Import de données preferences: Préférences settings: Réglages @@ -280,8 +383,11 @@ fr: show_more: Afficher plus visibilities: private: Abonné⋅es uniquement + private_long: Seul⋅es vos abonné⋅es verront vos status public: Public + public_long: Tout le monde peut voir vos status unlisted: Public sans être affiché sur le fil public + unlisted_long: Tout le monde peut voir vos status mais ils ne seront pas sur listés sur les fils publics stream_entries: click_to_show: Cliquer pour afficher reblogged: partagé @@ -294,11 +400,13 @@ fr: description_html: Si vous activez l'identification à deux facteurs, vous devrez être en possession de votre téléphone afin de générer un code de connexion. disable: Désactiver enable: Activer + enabled: L'authentification à deux facteurs est activée enabled_success: Identification à deux facteurs activée avec succès generate_recovery_codes: Générer les codes de récupération instructions_html: "Scannez ce QR code grâce à Google Authenticator, Authy ou une application similaire sur votre téléphone. Désormais, cette application générera des jetons que vous devrez saisir à chaque connexion." lost_recovery_codes: Les codes de récupération vous permettent de retrouver les accès à votre comptre si vous perdez votre téléphone. Si vous perdez vos codes de récupération, vous pouvez les générer à nouveau ici. Vos anciens codes de récupération seront invalidés. manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l''entrer manuellement, voici le secret en clair :' + recovery_codes: Codes de récupération recovery_codes_regenerated: Codes de récupération régénérés avec succès recovery_instructions_html: Si vous perdez l'accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer l'accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants. setup: Installer From 331f0953e9c1855b8195af756e83fee98600d1b8 Mon Sep 17 00:00:00 2001 From: Ratmir Karabut Date: Mon, 3 Jul 2017 02:30:22 +0300 Subject: [PATCH 028/114] Update Russian translation (sessions) (#4041) * Add Russian translation (ru) * Fix a missing comma * Fix the wording for better consistency * Update Russian translation * Arrange Russian setting alphabetically * Fix syntax error * Update Russian translation * Fix formatting error * Update Russian translation * Update Russian translation * Update ru.jsx * Fix syntax error * Remove two_factor_auth.warning (appears obsolete) * Add missing strings in ru.yml A lot of new strings translated, especially for the newly added admin section * Fix translation consistency * Update Russian translation * Update Russian translation (pluralizations) * Update Russian translation * Update Russian translation * Update Russian translation (pin) * Update Russian translation (account deletion) * Fix extra line * Update Russian translation (sessions) --- config/locales/ru.yml | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 9cf067d884d..6321e96eb0c 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -292,6 +292,43 @@ ru: missing_resource: Поиск требуемого перенаправления URL для Вашего аккаунта завершился неудачей proceed: Продолжить подписку prompt: 'Вы хотите подписаться на:' + sessions: + activity: Последняя активность + browser: Браузер + browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + firefox: Firefox + generic: Неизвестный браузер + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: Текущая сессия + description: "%{browser} на %{platform}" + explanation: Это веб-браузеры, в которых на данный момент выполнен вход в Ваш аккаунт Mastodon. + ip: IP + platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac + other: неизвестной платформе + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + title: Сессии settings: authorized_apps: Авторизованные приложения back: Назад в Mastodon From e28258010182b56f27cfbd3f9f9a58fd9cd8870d Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Mon, 3 Jul 2017 02:02:36 -0700 Subject: [PATCH 029/114] Faster emojify() by avoiding str.replace() entirely (#4049) --- app/javascript/mastodon/emoji.js | 69 ++++++++++------------ package.json | 1 + spec/javascript/components/emojify.test.js | 34 +++++++++++ yarn.lock | 4 ++ 4 files changed, 71 insertions(+), 37 deletions(-) diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js index d0df71ea378..7043d5f3a35 100644 --- a/app/javascript/mastodon/emoji.js +++ b/app/javascript/mastodon/emoji.js @@ -1,60 +1,55 @@ 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 => { - const mappedUnicode = emojione.mapUnicodeToShort(); - - return str.replace(emojione.regUnicode, unicodeChar => { - if (typeof unicodeChar === 'undefined' || unicodeChar === '' || !(unicodeChar in emojione.jsEscapeMap)) { - return unicodeChar; - } - - const unicode = emojione.jsEscapeMap[unicodeChar]; - const short = mappedUnicode[unicode]; - const filename = emojione.emojioneList[short].fname; - const alt = emojione.convert(unicode.toUpperCase()); - - return `${alt}`; - }); -}; - -const shortnameToImage = str => { - // This walks through the string from end to start, ignoring any tags (

,
, etc.) - // and replacing valid shortnames like :smile: and :wink: that _aren't_ within - // tags with an version. - // The goal is to be the same as an emojione.regShortNames replacement, but faster. - // The reason we go backwards is because then we can replace substrings as we go. - let i = str.length; +function emojify(str) { + // This walks through the string from start to end, ignoring any tags (

,
, etc.) + // and replacing valid shortnames like :smile: and :wink: as well as unicode strings + // that _aren't_ within tags with an version. + // The goal is to be the same as an emojione.regShortNames/regUnicode replacement, but faster. + let i = -1; let insideTag = false; let insideShortname = false; - let shortnameEndIndex = -1; - while (i--) { + let shortnameStartIndex = -1; + let match; + while (++i < str.length) { const char = str.charAt(i); if (insideShortname && char === ':') { - const shortname = str.substring(i, shortnameEndIndex + 1); + 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 = `${alt}`; - str = str.substring(0, i) + replacement + str.substring(shortnameEndIndex + 1); + 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 + i--; // stray colon, try again } insideShortname = false; - } else if (insideTag && char === '<') { + } else if (insideTag && char === '>') { insideTag = false; - } else if (char === '>') { + } else if (char === '<') { insideTag = true; insideShortname = false; } else if (!insideTag && char === ':') { insideShortname = true; - shortnameEndIndex = i; + 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 = `${alt}`; + 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 + } } } return str; -}; +} -export default function emojify(text) { - return toImage(text); -}; +export default emojify; diff --git a/package.json b/package.json index 7fa80a0c2fc..d5c05dae3bb 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "sass-loader": "^6.0.6", "stringz": "^0.2.2", "style-loader": "^0.18.2", + "substring-trie": "^1.0.0", "throng": "^4.0.0", "tiny-queue": "^0.2.1", "uuid": "^3.1.0", diff --git a/spec/javascript/components/emojify.test.js b/spec/javascript/components/emojify.test.js index 7a496623e55..3e8b25af935 100644 --- a/spec/javascript/components/emojify.test.js +++ b/spec/javascript/components/emojify.test.js @@ -46,4 +46,38 @@ describe('emojify', () => { expect(emojify(':smile')).to.equal(':smile'); }); + it('does two emoji next to each other', () => { + expect(emojify(':smile::wink:')).to.equal( + '😄😉'); + }); + + it('does unicode', () => { + expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).to.equal( + '👩‍👩‍👦‍👦'); + expect(emojify('\uD83D\uDC68\uD83D\uDC69\uD83D\uDC67\uD83D\uDC67')).to.equal( + '👨👩👧👧'); + expect(emojify('\uD83D\uDC69\uD83D\uDC69\uD83D\uDC66')).to.equal('👩👩👦'); + expect(emojify('\u2757')).to.equal( + '❗'); + }); + + it('does multiple unicode', () => { + expect(emojify('\u2757 #\uFE0F\u20E3')).to.equal( + '❗ #️⃣'); + expect(emojify('\u2757#\uFE0F\u20E3')).to.equal( + '❗#️⃣'); + expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).to.equal( + '❗ #️⃣ ❗'); + expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).to.equal( + 'foo ❗ #️⃣ bar'); + }); + + it('does mixed unicode and shortnames', () => { + expect(emojify(':smile:#\uFE0F\u20E3:wink:\u2757')).to.equal('😄#️⃣😉❗'); + }); + + it('ignores unicode inside of tags', () => { + expect(emojify('

')).to.equal('

'); + }); + }); diff --git a/yarn.lock b/yarn.lock index adabca08d19..609f256c922 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6819,6 +6819,10 @@ style-loader@^0.18.2: loader-utils "^1.0.2" schema-utils "^0.3.0" +substring-trie@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/substring-trie/-/substring-trie-1.0.0.tgz#5a7ecb83aefcca7b3720f7897cf69e97023be143" + sugarss@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/sugarss/-/sugarss-1.0.0.tgz#65e51b3958432fb70d5451a68bb33e32d0cf1ef7" From be94f9e35d4fbb1558fa772a618276562039fd96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AE=E3=82=89?= Date: Mon, 3 Jul 2017 18:02:59 +0900 Subject: [PATCH 030/114] Update Japanese translation (#4051) --- config/locales/ja.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 52752e7d185..b45e06d3a9b 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -385,7 +385,7 @@ ja: unlisted_long: 誰でも見ることができますが、公開タイムラインには表示されません stream_entries: click_to_show: クリックして表示 - reblogged: ブーストされました + reblogged: さんにブーストされました sensitive_content: 閲覧注意 time: formats: From a6d02cff368d96178b0843ef021232d2187abbcd Mon Sep 17 00:00:00 2001 From: abcang Date: Mon, 3 Jul 2017 18:03:34 +0900 Subject: [PATCH 031/114] Rescue exceptions caused by FetchLinkCardService (#4045) --- app/models/concerns/remotable.rb | 8 ++++++-- app/services/fetch_link_card_service.rb | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/models/concerns/remotable.rb b/app/models/concerns/remotable.rb index b0077ce96eb..08d4fc59cf2 100644 --- a/app/models/concerns/remotable.rb +++ b/app/models/concerns/remotable.rb @@ -10,7 +10,11 @@ module Remotable method_name = "#{attribute_name}=".to_sym define_method method_name do |url| - parsed_url = Addressable::URI.parse(url).normalize + begin + parsed_url = Addressable::URI.parse(url).normalize + rescue Addressable::URI::InvalidURIError + return + end return if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty? || self[attribute_name] == url @@ -26,7 +30,7 @@ module Remotable send("#{attachment_name}_file_name=", filename) self[attribute_name] = url if has_attribute?(attribute_name) - rescue HTTP::TimeoutError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError => e + rescue HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, Paperclip::Errors::NotIdentifiedByImageMagickError, Addressable::URI::InvalidURIError => e Rails.logger.debug "Error fetching remote #{attachment_name}: #{e}" nil end diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index c2df7b2f016..4ce221267d0 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -18,6 +18,8 @@ class FetchLinkCardService < BaseService return if res.code != 200 || res.mime_type != 'text/html' attempt_opengraph(card, url) unless attempt_oembed(card, url) + rescue HTTP::ConnectionError, OpenSSL::SSL::SSLError + nil end private From 92f1c474f305de2383a4d2b3dffbcc20800cfb6f Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 3 Jul 2017 18:04:35 +0900 Subject: [PATCH 032/114] Add fa-fw class to user agent icon (#4047) --- app/helpers/application_helper.rb | 8 ++++++-- app/views/auth/registrations/_sessions.html.haml | 7 ++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 36c37fae08f..9f50d8bdb32 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -31,7 +31,11 @@ module ApplicationHelper Rails.env.production? ? site_title : "#{site_title} (Dev)" end - def fa_icon(icon) - content_tag(:i, nil, class: 'fa ' + icon.split(' ').map { |cl| "fa-#{cl}" }.join(' ')) + def fa_icon(icon, attributes = {}) + 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 diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml index 11c0d4e315f..4521aad0a2b 100644 --- a/app/views/auth/registrations/_sessions.html.haml +++ b/app/views/auth/registrations/_sessions.html.haml @@ -11,9 +11,10 @@ - @sessions.each do |session| %tr %td - %span{ title: session.user_agent }= fa_icon session_device_icon(session) - = ' ' - = t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}") + %span{ title: session.user_agent }< + = fa_icon "#{session_device_icon(session)} fw", 'aria-label' => session_device_icon(session) + = ' ' + = t 'sessions.description', browser: t("sessions.browsers.#{session.browser}"), platform: t("sessions.platforms.#{session.platform}") %td %samp= session.ip %td From a9c326b200247c1dc943c3e2467381772d37bfc8 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 3 Jul 2017 18:56:58 +0900 Subject: [PATCH 033/114] Upgrade chai-enzyme, precss and uws (#4010) * Update chai-enzyme to v0.8.0 * Upgrade precss to v2.0.0 * Upgrade uws to v8.14.0 --- package.json | 6 +- yarn.lock | 257 ++++++++++++++++++++++----------------------------- 2 files changed, 116 insertions(+), 147 deletions(-) diff --git a/package.json b/package.json index d5c05dae3bb..0845e83dbdb 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "pg": "^6.4.0", "postcss-loader": "^2.0.6", "postcss-smart-import": "^0.7.4", - "precss": "^1.4.0", + "precss": "^2.0.0", "prop-types": "^15.5.10", "punycode": "^2.1.0", "rails-ujs": "^5.1.2", @@ -106,7 +106,7 @@ "throng": "^4.0.0", "tiny-queue": "^0.2.1", "uuid": "^3.1.0", - "uws": "^0.14.5", + "uws": "^8.14.0", "webpack": "^3.0.0", "webpack-bundle-analyzer": "^2.8.2", "webpack-manifest-plugin": "^1.1.0", @@ -118,7 +118,7 @@ "@storybook/react": "^3.1.6", "babel-eslint": "^7.2.3", "chai": "^4.0.1", - "chai-enzyme": "^0.7.1", + "chai-enzyme": "^0.8.0", "enzyme": "^2.9.1", "eslint": "^3.19.0", "eslint-plugin-jsx-a11y": "^4.0.0", diff --git a/yarn.lock b/yarn.lock index 609f256c922..a44f4ef7fda 100644 --- a/yarn.lock +++ b/yarn.lock @@ -255,7 +255,7 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -any-promise@^0.1.0, any-promise@~0.1.0: +any-promise@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-0.1.0.tgz#830b680aa7e56f33451d4b049f3bd8044498ee27" @@ -1317,10 +1317,6 @@ balanced-match@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.1.0.tgz#b504bd05869b39259dd0c5efc35d843176dccc4a" -balanced-match@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.2.1.tgz#7bc658b4bed61eee424ad74f75f5c3e2c4df3cc7" - balanced-match@^0.4.2: version "0.4.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" @@ -1520,6 +1516,10 @@ callsites@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" +camelcase-css@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-1.0.1.tgz#157c4238265f5cf94a1dffde86446552cbf3f705" + camelcase-keys@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" @@ -1575,9 +1575,9 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -chai-enzyme@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/chai-enzyme/-/chai-enzyme-0.7.1.tgz#a945c81989bcc4fd96af6263f9c0a9c668f29b66" +chai-enzyme@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/chai-enzyme/-/chai-enzyme-0.8.0.tgz#609c552a1dcdb091f435e1e281cc4f2149a33be1" dependencies: html "^1.0.0" react-element-to-jsx-string "^5.0.0" @@ -1996,7 +1996,7 @@ crypto-random-string@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" -css-color-function@^1.2.0: +css-color-function@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/css-color-function/-/css-color-function-1.3.0.tgz#72c767baf978f01b8a8a94f42f17ba5d22a776fc" dependencies: @@ -3001,15 +3001,6 @@ fresh@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" -fs-extra@^0.24.0: - version "0.24.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.24.0.tgz#d4e4342a96675cb7846633a6099249332b539952" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - path-is-absolute "^1.0.0" - rimraf "^2.2.8" - fs-extra@^0.30.0: version "0.30.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0" @@ -3020,11 +3011,9 @@ fs-extra@^0.30.0: path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-promise@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/fs-promise/-/fs-promise-0.3.1.tgz#bf34050368f24d6dc9dfc6688ab5cead8f86842a" - dependencies: - any-promise "~0.1.0" +fs-readdir-recursive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" fs.realpath@^1.0.0: version "1.0.0" @@ -3175,16 +3164,6 @@ glob@7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^5.0.3: - version "5.0.15" - resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@~7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" @@ -3207,17 +3186,6 @@ globals@^9.0.0, globals@^9.14.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" -globby@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-3.0.1.tgz#2094af8421e19152150d5893eb6416b312d9a22f" - dependencies: - array-union "^1.0.1" - arrify "^1.0.0" - glob "^5.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^1.0.0" - globby@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" @@ -4405,7 +4373,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" -"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: +minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" dependencies: @@ -5019,22 +4987,12 @@ pify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" -pinkie-promise@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-1.0.0.tgz#d1da67f5482563bb7cf57f286ae2822ecfbf3670" - dependencies: - pinkie "^1.0.0" - pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" dependencies: pinkie "^2.0.0" -pinkie@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-1.0.0.tgz#5a47f28ba1015d0201bda7bf0f358e47bec8c7e4" - pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" @@ -5074,7 +5032,7 @@ postcss-advanced-variables@1.2.2: dependencies: postcss "^5.0.10" -postcss-atroot@^0.1.2: +postcss-atroot@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/postcss-atroot/-/postcss-atroot-0.1.3.tgz#6752c0230c745140549345b2b0e30ebeda01a405" dependencies: @@ -5088,12 +5046,12 @@ postcss-calc@^5.2.0: postcss-message-helpers "^2.0.0" reduce-css-calc "^1.2.6" -postcss-color-function@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-2.0.1.tgz#9ad226f550e8a7c7f8b8a77860545b6dd7f55241" +postcss-color-function@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-function/-/postcss-color-function-4.0.0.tgz#7e0106f4f6a1ecb1ad5b3a8553ace5e828aae187" dependencies: - css-color-function "^1.2.0" - postcss "^5.0.4" + css-color-function "^1.3.0" + postcss "^6.0.1" postcss-message-helpers "^2.0.0" postcss-value-parser "^3.3.0" @@ -5112,26 +5070,25 @@ postcss-convert-values@^2.3.4: postcss "^5.0.11" postcss-value-parser "^3.1.2" -postcss-custom-media@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-5.0.1.tgz#138d25a184bf2eb54de12d55a6c01c30a9d8bd81" +postcss-custom-media@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-6.0.0.tgz#be532784110ecb295044fb5395a18006eb21a737" dependencies: - postcss "^5.0.0" + postcss "^6.0.1" -postcss-custom-properties@^5.0.0: - version "5.0.2" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-5.0.2.tgz#9719d78f2da9cf9f53810aebc23d4656130aceb1" +postcss-custom-properties@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-6.1.0.tgz#9caf1151ac41b1e9e64d3a2ff9ece996ca18977d" dependencies: - balanced-match "^0.4.2" - postcss "^5.0.0" + balanced-match "^1.0.0" + postcss "^6.0.3" -postcss-custom-selectors@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-3.0.0.tgz#8f81249f5ed07a8d0917cf6a39fe5b056b7f96ac" +postcss-custom-selectors@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-4.0.1.tgz#781382f94c52e727ef5ca4776ea2adf49a611382" dependencies: - balanced-match "^0.2.0" - postcss "^5.0.0" - postcss-selector-matches "^2.0.0" + postcss "^6.0.1" + postcss-selector-matches "^3.0.0" postcss-discard-comments@^2.0.4: version "2.0.4" @@ -5164,7 +5121,7 @@ postcss-discard-unused@^2.2.1: postcss "^5.0.14" uniqs "^2.0.0" -postcss-extend@^1.0.1: +postcss-extend@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/postcss-extend/-/postcss-extend-1.0.5.tgz#5ea98bf787ba3cacf4df4609743f80a833b1d0e7" dependencies: @@ -5183,6 +5140,23 @@ postcss-flexbugs-fixes@^3.0.0: dependencies: postcss "^6.0.1" +postcss-import@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-10.0.0.tgz#4c85c97b099136cc5ea0240dc1dfdbfde4e2ebbe" + dependencies: + object-assign "^4.0.1" + postcss "^6.0.1" + postcss-value-parser "^3.2.3" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-1.0.0.tgz#ccee5aa3b1970dd457008e79438165f66919ba30" + dependencies: + camelcase-css "^1.0.1" + postcss "^6.0.1" + postcss-load-config@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-1.2.0.tgz#539e9afc9ddc8620121ebf9d8c3673e0ce50d28a" @@ -5215,11 +5189,11 @@ postcss-loader@^2.0.5, postcss-loader@^2.0.6: postcss-load-config "^1.2.0" schema-utils "^0.3.0" -postcss-media-minmax@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-2.1.2.tgz#444c5cf8926ab5e4fd8a2509e9297e751649cdf8" +postcss-media-minmax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-media-minmax/-/postcss-media-minmax-3.0.0.tgz#675256037a43ef40bc4f0760bfd06d4dc69d48d2" dependencies: - postcss "^5.0.4" + postcss "^6.0.1" postcss-merge-idents@^2.1.5: version "2.1.7" @@ -5282,13 +5256,15 @@ postcss-minify-selectors@^2.0.4: postcss "^5.0.14" postcss-selector-parser "^2.0.0" -postcss-mixins@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-2.1.1.tgz#b141a0803efa8e2d744867f8d91596890cf9241b" +postcss-mixins@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-mixins/-/postcss-mixins-6.0.1.tgz#f5c9726259a6103733b43daa6a8b67dd0ed7aa47" dependencies: - globby "^3.0.1" - postcss "^5.0.10" - postcss-simple-vars "^1.0.1" + globby "^6.1.0" + postcss "^6.0.3" + postcss-js "^1.0.0" + postcss-simple-vars "^4.0.0" + sugarss "^1.0.0" postcss-modules-extract-imports@^1.0.0: version "1.2.0" @@ -5317,17 +5293,17 @@ postcss-modules-values@^1.1.0: icss-replace-symbols "^1.1.0" postcss "^6.0.1" -postcss-nested@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-1.0.1.tgz#91f28f4e6e23d567241ac154558a0cfab4cc0d8f" +postcss-nested@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-2.0.2.tgz#f38fad547f5c3747160aec3bb34745819252974a" dependencies: - postcss "^5.2.17" + postcss "^6.0.1" -postcss-nesting@^2.0.6: - version "2.3.1" - resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-2.3.1.tgz#94a6b6a4ef707fbec20a87fee5c957759b4e01cf" +postcss-nesting@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-4.0.1.tgz#8fc2ce40cbfcfab7ee24e7b68fb6ebe84b641469" dependencies: - postcss "^5.0.19" + postcss "^6.0.1" postcss-normalize-charset@^1.1.0: version "1.1.1" @@ -5351,17 +5327,14 @@ postcss-ordered-values@^2.1.0: postcss "^5.0.4" postcss-value-parser "^3.0.1" -postcss-partial-import@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/postcss-partial-import/-/postcss-partial-import-1.3.0.tgz#2f4b773a76c7b0a69b389dcf475c4d362d0d2576" +postcss-partial-import@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-partial-import/-/postcss-partial-import-4.1.0.tgz#f6c3e78e7bbeda4d9dab96d360367b90b353f9a4" dependencies: - fs-extra "^0.24.0" - fs-promise "^0.3.1" - object-assign "^4.0.1" - postcss "^5.0.5" - string-hash "^1.1.0" + glob "^7.1.1" + postcss-import "^10.0.0" -postcss-property-lookup@^1.1.3: +postcss-property-lookup@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/postcss-property-lookup/-/postcss-property-lookup-1.2.1.tgz#30450a1361b7aae758bbedd5201fbe057bb8270b" dependencies: @@ -5404,19 +5377,19 @@ postcss-scss@^1.0.0: dependencies: postcss "^6.0.3" -postcss-selector-matches@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-2.0.5.tgz#fa0f43be57b68e77aa4cd11807023492a131027f" +postcss-selector-matches@^3.0.0, postcss-selector-matches@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-matches/-/postcss-selector-matches-3.0.1.tgz#e5634011e13950881861bbdd58c2d0111ffc96ab" dependencies: balanced-match "^0.4.2" - postcss "^5.0.0" + postcss "^6.0.1" -postcss-selector-not@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-2.0.0.tgz#c73ad21a3f75234bee7fee269e154fd6a869798d" +postcss-selector-not@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-3.0.1.tgz#2e4db2f0965336c01e7cec7db6c60dff767335d9" dependencies: - balanced-match "^0.2.0" - postcss "^5.0.0" + balanced-match "^0.4.2" + postcss "^6.0.1" postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: version "2.2.3" @@ -5426,11 +5399,11 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2: indexes-of "^1.0.1" uniq "^1.0.1" -postcss-simple-vars@^1.0.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-1.2.0.tgz#2e6689921144b74114e765353275a3c32143f150" +postcss-simple-vars@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-simple-vars/-/postcss-simple-vars-4.0.0.tgz#d49e082897d9a4824f2268fa91d969d943e2ea76" dependencies: - postcss "^5.0.13" + postcss "^6.0.1" postcss-smart-import@^0.7.4: version "0.7.4" @@ -5477,7 +5450,7 @@ postcss-zindex@^2.0.1: postcss "^5.0.4" uniqs "^2.0.0" -postcss@^5.0.0, postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.19, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16, postcss@^5.2.17, postcss@^5.2.6: +postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16, postcss@^5.2.6: version "5.2.17" resolved "https://registry.yarnpkg.com/postcss/-/postcss-5.2.17.tgz#cf4f597b864d65c8a492b2eabe9d706c879c388b" dependencies: @@ -5516,26 +5489,26 @@ precond@0.2: version "0.2.3" resolved "https://registry.yarnpkg.com/precond/-/precond-0.2.3.tgz#aa9591bcaa24923f1e0f4849d240f47efc1075ac" -precss@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/precss/-/precss-1.4.0.tgz#8d7c3ae70f10a00a3955287f85a66e0f8b31cda3" +precss@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/precss/-/precss-2.0.0.tgz#7f567e3318e06d44c8fdbf9e58452e8358bf4b71" dependencies: - postcss "^5.0.10" + postcss "^6.0.3" postcss-advanced-variables "1.2.2" - postcss-atroot "^0.1.2" - postcss-color-function "^2.0.0" - postcss-custom-media "^5.0.0" - postcss-custom-properties "^5.0.0" - postcss-custom-selectors "^3.0.0" - postcss-extend "^1.0.1" - postcss-media-minmax "^2.1.0" - postcss-mixins "^2.1.0" - postcss-nested "^1.0.0" - postcss-nesting "^2.0.6" - postcss-partial-import "^1.3.0" - postcss-property-lookup "^1.1.3" - postcss-selector-matches "^2.0.0" - postcss-selector-not "^2.0.0" + postcss-atroot "^0.1.3" + postcss-color-function "^4.0.0" + postcss-custom-media "^6.0.0" + postcss-custom-properties "^6.1.0" + postcss-custom-selectors "^4.0.1" + postcss-extend "^1.0.5" + postcss-media-minmax "^3.0.0" + postcss-mixins "^6.0.1" + postcss-nested "^2.0.2" + postcss-nesting "^4.0.1" + postcss-partial-import "^4.1.0" + postcss-property-lookup "^1.2.1" + postcss-selector-matches "^3.0.1" + postcss-selector-not "^3.0.1" prelude-ls@~1.1.2: version "1.1.2" @@ -6254,7 +6227,7 @@ resolve-url@~0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" -resolve@^1.1.6, resolve@^1.3.3: +resolve@^1.1.6, resolve@^1.1.7, resolve@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.3.tgz#655907c3469a8680dc2de3a275a8fdd69691f0e5" dependencies: @@ -6710,10 +6683,6 @@ strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" -string-hash@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" - string-width@^1.0.1, string-width@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -7145,9 +7114,9 @@ uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" -uws@^0.14.5: - version "0.14.5" - resolved "https://registry.yarnpkg.com/uws/-/uws-0.14.5.tgz#67aaf33c46b2a587a5f6666d00f7691328f149dc" +uws@^8.14.0: + version "8.14.0" + resolved "https://registry.yarnpkg.com/uws/-/uws-8.14.0.tgz#acc1488d13ecb23fe2f942a7eafb06681fa91431" validate-npm-package-license@^3.0.1: version "3.0.1" From f85dbe83c8e982f9685fbc802031b74c7c319bc7 Mon Sep 17 00:00:00 2001 From: "Akihiko Odaki (@fn_aki@pawoo.net)" Date: Mon, 3 Jul 2017 20:17:27 +0900 Subject: [PATCH 034/114] Remove sort in Feed (#4050) In from_redis method, statuses retrieved from the database was mapped to the IDs retrieved from Redis. It was equivalent to order from high to low because those IDs are sorted in the same order. Statuses are ordered with the ID by default, so we do not have to reorder. Sorting statuses in the database is even faster since the IDs are indexed with B-tree. --- app/models/feed.rb | 3 +-- spec/models/feed_spec.rb | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/models/feed.rb b/app/models/feed.rb index 5125e51ff82..beb4a8de3fc 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -20,8 +20,7 @@ class Feed max_id = '+inf' if max_id.blank? since_id = '-inf' if since_id.blank? unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).map(&:last).map(&:to_i) - status_map = Status.where(id: unhydrated).cache_ids.map { |s| [s.id, s] }.to_h - unhydrated.map { |id| status_map[id] }.compact + Status.where(id: unhydrated).cache_ids end def from_database(limit, max_id, since_id) diff --git a/spec/models/feed_spec.rb b/spec/models/feed_spec.rb index 1cdb3a7839f..1c377c17f39 100644 --- a/spec/models/feed_spec.rb +++ b/spec/models/feed_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' RSpec.describe Feed, type: :model do describe '#get' do - it 'gets statuses with ids in the range, maintining the order from Redis' do + it 'gets statuses with ids in the range' do account = Fabricate(:account) Fabricate(:status, account: account, id: 1) Fabricate(:status, account: account, id: 2) From 275c5b51ed7e22734d18db6acb2b87ba26bd435f Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Tue, 4 Jul 2017 22:19:24 +0900 Subject: [PATCH 035/114] Customizable privacy policy from admin interface (#4062) --- app/controllers/about_controller.rb | 2 +- app/controllers/admin/settings_controller.rb | 1 + app/presenters/instance_presenter.rb | 1 + app/views/about/terms.en.html.haml | 76 -------------------- app/views/about/terms.html.haml | 8 +++ app/views/about/terms.ja.html.haml | 76 -------------------- app/views/about/terms.no.html.haml | 76 -------------------- app/views/admin/settings/edit.html.haml | 7 ++ config/locales/en.yml | 73 +++++++++++++++++++ config/locales/ja.yml | 70 ++++++++++++++++++ config/locales/no.yml | 70 ++++++++++++++++++ config/settings.yml | 1 + 12 files changed, 232 insertions(+), 229 deletions(-) delete mode 100644 app/views/about/terms.en.html.haml create mode 100644 app/views/about/terms.html.haml delete mode 100644 app/views/about/terms.ja.html.haml delete mode 100644 app/views/about/terms.no.html.haml diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 04e7ddacf75..c0addbeccc6 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -2,7 +2,7 @@ class AboutController < ApplicationController 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 diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index fcd42c79c8c..7542f55e8b3 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -8,6 +8,7 @@ module Admin site_title site_description site_extended_description + site_terms open_registrations closed_registrations_message ).freeze diff --git a/app/presenters/instance_presenter.rb b/app/presenters/instance_presenter.rb index 9a69809d0e5..63ef23d5d73 100644 --- a/app/presenters/instance_presenter.rb +++ b/app/presenters/instance_presenter.rb @@ -7,6 +7,7 @@ class InstancePresenter :open_registrations, :site_description, :site_extended_description, + :site_terms, to: Setting ) diff --git a/app/views/about/terms.en.html.haml b/app/views/about/terms.en.html.haml deleted file mode 100644 index 7e0fb94c213..00000000000 --- a/app/views/about/terms.en.html.haml +++ /dev/null @@ -1,76 +0,0 @@ -- content_for :page_title do - #{site_hostname} Terms of Service and Privacy Policy - -.wrapper - %h2 Privacy Policy - - %h3#collect What information do we collect? - - %p We collect information from you when you register on our site and gather data when you participate in the forum by reading, writing, and evaluating the content shared here. - - %p When registering on our site, you may be asked to enter your name and e-mail address. You may, however, visit our site without registering. Your e-mail address will be verified by an email containing a unique link. If that link is visited, we know that you control the e-mail address. - - %p When registered and posting, we record the IP address that the post originated from. We also may retain server logs which include the IP address of every request to our server. - - %h3#use What do we use your information for? - - %p Any of the information we collect from you may be used in one of the following ways: - - %ul - %li To personalize your experience — your information helps us to better respond to your individual needs. - %li To improve our site — we continually strive to improve our site offerings based on the information and feedback we receive from you. - %li To improve customer service — your information helps us to more effectively respond to your customer service requests and support needs. - %li To send periodic emails — The email address you provide may be used to send you information, notifications that you request about changes to topics or in response to your user name, respond to inquiries, and/or other requests or questions. - - %h3#protect How do we protect your information? - - %p We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. - - %h3#data-retention What is your data retention policy? - - %p We will make a good faith effort to: - - %ul - %li Retain server logs containing the IP address of all requests to this server no more than 90 days. - %li Retain the IP addresses associated with registered users and their posts no more than 5 years. - - %h3#cookies Do we use cookies? - - %p Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account. - - %p We use cookies to understand and save your preferences for future visits and compile aggregate data about site traffic and site interaction so that we can offer better site experiences and tools in the future. We may contract with third-party service providers to assist us in better understanding our site visitors. These service providers are not permitted to use the information collected on our behalf except to help us conduct and improve our business. - - %h3#disclose Do we disclose any information to outside parties? - - %p We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety. However, non-personally identifiable visitor information may be provided to other parties for marketing, advertising, or other uses. - - %h3#third-party Third party links - - %p Occasionally, at our discretion, we may include or offer third party products or services on our site. These third party sites have separate and independent privacy policies. We therefore have no responsibility or liability for the content and activities of these linked sites. Nonetheless, we seek to protect the integrity of our site and welcome any feedback about these sites. - - %h3#coppa Children's Online Privacy Protection Act Compliance - - %p - Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA - = surround '(', '),' do - = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act' - do not use this site. - - %h3#online Online Privacy Policy Only - - %p This online privacy policy applies only to information collected through our site and not to information collected offline. - - %h3#consent Your Consent - - %p By using our site, you consent to our web site privacy policy. - - %h3#changes Changes to our Privacy Policy - - %p If we decide to change our privacy policy, we will post those changes on this page. - - %p This document is CC-BY-SA. It was last updated May 31, 2013. - - %p - Originally adapted from the - = succeed '.' do - = link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse' diff --git a/app/views/about/terms.html.haml b/app/views/about/terms.html.haml new file mode 100644 index 00000000000..58064f0bed9 --- /dev/null +++ b/app/views/about/terms.html.haml @@ -0,0 +1,8 @@ +- content_for :page_title do + = t('terms.title', instance: site_hostname) + +.wrapper + - if @instance_presenter.site_terms.present? + = raw @instance_presenter.site_terms + - else + = t('terms.body_html') diff --git a/app/views/about/terms.ja.html.haml b/app/views/about/terms.ja.html.haml deleted file mode 100644 index 5c546b3e0e9..00000000000 --- a/app/views/about/terms.ja.html.haml +++ /dev/null @@ -1,76 +0,0 @@ -- content_for :page_title do - #{site_hostname} 利用規約・プライバシーポリシー - -.wrapper - %h2 プライバシーポリシー - - %h3#collect どのような情報を収集するのですか? - - %p あなたがこのサイトに登録すると、ここで共有された情報を読んだり、書いたり、評価したりして、フォーラムでの情報を集める事ができます。 - - %p このサイトに登録する際には、名前とメールアドレスの入力を求めることがあります。ただし、登録をすることなくこのサイトを利用することも可能です。あなたのメールアドレスは、固有のリンクを含んだメールで確認されます。そのリンクにアクセスした場合にメールアドレスを制御することとなります。 - - %p アカウントを登録し、投稿を行った際にはその投稿が行われたIPアドレスを記録します。また、このサーバーに対する全てのリクエストはIPアドレスを含むサーバーログとして保管されます。 - - %h3#use 自分の情報を何に使うのですか? - - %p このサイトで収集された情報は、次のいくつかの方法で使用されます: - - %ul - %li パーソナライズ・エクスペリエンス — あなたの情報は、あなたや他のユーザーのニーズに対応するために役立ちます。 - %li サイトの改善・最適化 — このサービスはあなたから受け取った情報やフィードバックに基づいて提供されるサイトの改善を行いつづけます。 - %li サービスの向上 — あなたの情報は、ユーザーからの要求やサポートへより効果的に対応するために役立ちます。 - %li 定期メールの送信 — メールアドレスは、情報の送信、トピックの変更やユーザー名に関係するお知らせ、お問い合わせに関する返答、その他のリクエストや質問に関してお知らせするために使用されます。 - - %h3#protect 自分の情報はどのように保護されるのですか? - - %p このサービスはあなたの個人情報の入力、送信、またはアクセスに際してあなたの個人情報の安全性を維持するために様々なセキュリティ手段をとっています。 - - %h3#data-retention データ保持のポリシーはどのようになっていますか? - - %p このサービスはデータ保持に関して次のことを行うよう努めます。: - - %ul - %li このサーバーへのすべての要求に対して、IPアドレスを含むサーバーログを90日以内に渡って保持します。 - %li 登録されたユーザーとその投稿に関連付けされたIPアドレスを5年以内に渡って保持します。 - - %h3#cookies クッキーを使用していますか? - - %p はい。クッキーはあなたがウェブブラウザ上で許可した場合にコンピュータのストレージに転送される小さなファイルです。これらのクッキーを使用すると、サイトでブラウザが識別され、登録済みのアカウントを持っている場合は登録済みのアカウントに関連付けがされます。 - - %p クッキーを使用して、今後再度閲覧された場合に前回のデータから設定を呼び出したり、今後の改善のためにサイトのトラフィックやサイトの相互作用に関する集計データを作成します。このサービスは、サイトを訪れた方との理解を深めるために、第三者のサービス提供者と契約することがあります。これらのサービス提供者というものは、このサービスでの業務を行ったり、改善するためにこのサービスの代わって収集された情報を使用することはできません。 - - %h3#disclose このサイトは外部に何らかの情報を開示していますか? - - %p 私たちは、個人を特定出来る情報を外部へ販売、取引、または他の方法で渡すことはありません。これには、このサイトを操作したり、業務を行ったり、サービスを提供するのに役立つ信頼できる第三者は含まれません。法令遵守、サイトポリシーの施行、このサービスや他の人の権利、財産または安全の保護のために適切であると判断した場合に、あなたの情報を公開する場合があります。ただし、マーケティングや広告、その他の目的で匿名での訪問者情報を他者へ提供することができます。 - - %h3#third-party サードパーティのリンク - - %p 必要に応じて、このサービスの方針にもとづいてこのサイトや第三者のサービスを提供することがあります。これらの第三者のサイトには、個別の独立したプライバシーポリシーがあります。従って、これらのリンク先のサイトに関するコンテンツや活動にかんしては一切責任を負いません。ですが、サイトの完全性やこれらのサイトに関するフィードバックは非常に重要なものであると認識しております。 - - %h3#coppa 子供のオンライン・プライバシー保護法 - - %p - このサイト、製品、サービスはすべて13歳以上の人を対象としております。このサーバーが米国にあり、13歳未満の場合はCOPPA - = surround '(', '),' do - = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act' - にもとづいてこのサイトを使用しないでください。 - - %h3#online オンライン限定のプライバシーポリシー - - %p このオンライン・プライバシーポリシーは、このサイトを通じて収集された情報のみに適用され、オフラインで収集される情報には適用されません。 - - %h3#consent あなたの同意 - - %p このサービスを使用することにより、このサイトのプライバシーポリシーに同意するものとします。 - - %h3#changes プライバシーポリシーの変更 - - %p プライバシーポリシーを変更する場合は、このページへ変更内容を掲載します。 - - %p この文章のライセンスはCC-BY-SAです。このページは2017年5月6日が最終更新です。 - - %p - オリジナルの出典 - = succeed '.' do - = link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse' diff --git a/app/views/about/terms.no.html.haml b/app/views/about/terms.no.html.haml deleted file mode 100644 index 46f62950df1..00000000000 --- a/app/views/about/terms.no.html.haml +++ /dev/null @@ -1,76 +0,0 @@ -- content_for :page_title do - #{site_hostname} Personvern og villkår for bruk av nettstedet - -.wrapper - %h2 Personvernserklæring - - %h3#collect Hvilke opplysninger samler vi? - - %p Vi samler opplysninger fra deg når du registrerer deg på nettstedet vårt, og vi samler data når du deltar på forumet ved å lese, skrive og evaluere innholdet som deles her. - - %p Når du registrerer deg på nettstedet vårt, kan du bli bedt om å oppgi navnet og e-postadressen din. Imidlertid kan du besøke nettstedet vårt uten å registrere deg. E-postadressen din vil bli bekreftet med en e-post som inneholder en unik lenke. Hvis siden den lenker til, blir besøkt, vet vi at du har kontroll over e-postadressen. - - %p Når du registrerer deg og skriver innlegg, registrerer vi IP-adressen som innlegget stammer fra. Vi kan også oppbevare logger som inkluderer IP-adressen til alle forespørslene sendt til tjeneren vår. - - %h3#use Hva bruker vi opplysningene dine til? - - %p Alle opplysningene vi samler fra deg, kan bli brukt på en av følgende måter: - - %ul - %li For å gjøre opplevelsen din mer personlig. Opplysningene dine hjelper oss å svare bedre på dine individuelle behov. - %li For å forbedre nettstedet vårt. Vi jobber konstant for å forbedre nettstedets tilbud basert på opplysningene og tilbakemeldingene vi mottar fra deg. - %li For å forbedre vår kundeservice. Dine opplysninger hjelper oss å svare mer effektivt på dine forespørsler sendt til kundeservice eller behov om støtte. - %li For å sende periodiske e-poster. E-postadressen du oppgir, kan bli brukt til å sende deg informasjon, påminnelser som du ber om ved endringer av emner eller ved svar til brukernavnet ditt, til henvendelser, og/eller andre forspørsler eller andre spørsmål. - - %h3#protect Hvordan sikrer vi opplysningene? - - %p Vi gjennomfører flere sikkerhetstiltak for å holde personopplysningene dine sikre når du skriver inn, lagrer eller henter dem. - - %h3#data-retention Hva er retningslinjene deres for lagring av data? - - %p Vi vil forsøke i god tro å: - - %ul - %li Ikke oppbevare tjener-logger som inneholder IP-adressen til alle forespørslene til denne tjeneren i lenger enn i 90 dager. - %li Ikke oppbevare IP-adressene forbundet med registrerte brukere og deres innlegg lenger enn i 5 år. - - %h3#cookies Bruker vi informasjonskapsler? - - %p Ja. Informasjonskapsler er små filer som et nettsted eller dets tjenesteleverandør overfører til harddisken på datamaskinen din gjennom nettleseren din (dersom du tillater det). Disse informasjonskapslene gjør det mulig for nettstedet å gjenkjenne nettleseren din og, dersom du har en konto, knytte nettleseren til den. - - %p Vi bruker informasjonskapsler for å forstå og lagre preferansene dine for fremtidige besøk og for å samle aggregatdata om trafikk på og samhandling med nettstedet slik at vi kan tilby bedre opplevelser og verktøy på nettstedet i fremtiden. Vi kan inngå avtaler med tredjeparts tjenesteleverandører for å bistå oss i å forstå besøkerne våres bedre. Disse tjenesteleverandørene har ikke lov til å bruke opplysningene samlet på våres vegne unntatt til å hjelpe oss å gjennomføre og forbedre anliggendet vårt. - - %h3#disclose Gir vi noen opplysninger videre til andre parter? - - %p Vi verken selger, handler med eller overfører på noen annen måte til andre parter dine identifiserbare personopplysninger. Dette inkluderer ikke tredjeparter som har vår tillit og bistår oss i å drive nettstedet, utføre våre anliggender eller yter tjenester til deg, så lenge disse partene samtykker til å behandle disse opplysningene fortrolig. Vi kan også frigi opplysningene dine dersom vi tror at å frigi dem er hensiktsmessig for å overholde loven, håndheve nettstedet retningslinjer eller beskytte våre og andres rettigheter. Imidlertid kan opplysninger som ikke er personlig identifiserbare, bli delt med andre parter for markedsføring, reklame eller annet bruk. - - %h3#third-party Tredjeparts lenker - - %p Av og til, etter skjønn, kan vil inkludere eller tilby tredjeparts produkter eller tjenester på nettstedet vårt. Disse tredjeparts nettstedene har separate og selvstendige personvernerklæringer. Vi bærer derfor intet ansvar eller forpliktelser for innholdet eller aktivitetene til disse nettstedene det lenkes til. Ikke mindre prøver vi å bevare vår eget nettsteds integritet og ønsker enhver tilbakemelding om disse nettstedene velkomne. - - %h3#coppa Overensstemmelse med Children's Online Privacy Protection Act - - %p - Nettstedet er rettet mot folk som er minst 13 år gamle. Dersom denne tjeneren er i USA, og du er under 13 år i henhold til kravene i COPPA - = surround '(', '),' do - = link_to 'Children\'s Online Privacy Protection Act', 'https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act' - ikke bruk dette nettstedet. - - %h3#online Personvernerklæring bare for nettet - - %p Denne nett-personvernerklæringen gjelder bare for informasjon samlet gjennom nettstedet vårt og ikke for opplysninger samlet når en er frakoblet. - - %h3#consent Ditt samtykke - - %p Ved å bruke dette nettstedet samtykker du til nettstedets personvernerklæring. - - %h3#changes Endringer i vår personvernerklæring - - %p Dersom vi beslutter å endre personvernerklæringen vår, vil vi publisere disse endringene på denne siden. - - %p Dette dokumentet er lisensiert under CC-BY-SA. De ble sist oppdatert 12. april 2017. - - %p - Dokumentet er en adoptert og endret versjon fra - = succeed '.' do - = link_to 'Discourse privacy policy', 'https://github.com/discourse/discourse' diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml index edb69e36033..3096a958da6 100644 --- a/app/views/admin/settings/edit.html.haml +++ b/app/views/admin/settings/edit.html.haml @@ -40,6 +40,13 @@ %td= text_area_tag :site_extended_description, @settings['site_extended_description'].value, rows: 8 + %tr + %td + %strong= t('admin.settings.site_terms.title') + %p= t('admin.settings.site_terms.desc_html') + %td= text_area_tag :site_terms, + @settings['site_terms'].value, + rows: 8 %tr %td %strong= t('admin.settings.registrations.open.title') diff --git a/config/locales/en.yml b/config/locales/en.yml index 944c24c60c8..60e192491e1 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -183,6 +183,9 @@ en: site_description_extended: desc_html: Displayed on extended information page
You can use HTML tags title: Extended site description + site_terms: + desc_html: Displayed on terms page
You can use HTML tags + title: Site Privacy Policy site_title: Site title title: Site Settings subscriptions: @@ -387,6 +390,76 @@ en: click_to_show: Click to show reblogged: boosted sensitive_content: Sensitive content + terms: + body_html: | +

Privacy Policy

+ +

What information do we collect?

+ +

We collect information from you when you register on our site and gather data when you participate in the forum by reading, writing, and evaluating the content shared here.

+ +

When registering on our site, you may be asked to enter your name and e-mail address. You may, however, visit our site without registering. Your e-mail address will be verified by an email containing a unique link. If that link is visited, we know that you control the e-mail address.

+ +

When registered and posting, we record the IP address that the post originated from. We also may retain server logs which include the IP address of every request to our server.

+ +

What do we use your information for?

+ +

Any of the information we collect from you may be used in one of the following ways:

+ +
    +
  • To personalize your experience — your information helps us to better respond to your individual needs.
  • +
  • To improve our site — we continually strive to improve our site offerings based on the information and feedback we receive from you.
  • +
  • To improve customer service — your information helps us to more effectively respond to your customer service requests and support needs.
  • +
  • To send periodic emails — The email address you provide may be used to send you information, notifications that you request about changes to topics or in response to your user name, respond to inquiries, and/or other requests or questions.
  • +
+ +

How do we protect your information?

+ +

We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information.

+ +

What is your data retention policy?

+ +

We will make a good faith effort to:

+ +
    +
  • Retain server logs containing the IP address of all requests to this server no more than 90 days.
  • +
  • Retain the IP addresses associated with registered users and their posts no more than 5 years.
  • +
+ +

Do we use cookies?

+ +

Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account.

+ +

We use cookies to understand and save your preferences for future visits and compile aggregate data about site traffic and site interaction so that we can offer better site experiences and tools in the future. We may contract with third-party service providers to assist us in better understanding our site visitors. These service providers are not permitted to use the information collected on our behalf except to help us conduct and improve our business.

+ +

Do we disclose any information to outside parties?

+ +

We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety. However, non-personally identifiable visitor information may be provided to other parties for marketing, advertising, or other uses.

+ +

Third party links

+ +

Occasionally, at our discretion, we may include or offer third party products or services on our site. These third party sites have separate and independent privacy policies. We therefore have no responsibility or liability for the content and activities of these linked sites. Nonetheless, we seek to protect the integrity of our site and welcome any feedback about these sites.

+ +

Children's Online Privacy Protection Act Compliance

+ +

Our site, products and services are all directed to people who are at least 13 years old. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA (Children's Online Privacy Protection Act) do not use this site.

+ +

Online Privacy Policy Only

+ +

This online privacy policy applies only to information collected through our site and not to information collected offline.

+ + + +

By using our site, you consent to our web site privacy policy.

+ +

Changes to our Privacy Policy

+ +

If we decide to change our privacy policy, we will post those changes on this page.

+ +

This document is CC-BY-SA. It was last updated May 31, 2013.

+ +

Originally adapted from the Discourse privacy policy. + title: "%{instance} Terms of Service and Privacy Policy" time: formats: default: "%b %d, %Y, %H:%M" diff --git a/config/locales/ja.yml b/config/locales/ja.yml index b45e06d3a9b..347270af243 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -387,6 +387,76 @@ ja: click_to_show: クリックして表示 reblogged: さんにブーストされました sensitive_content: 閲覧注意 + terms: + body_html: | +

プライバシーポリシー

+ +

どのような情報を収集するのですか?

+ +

あなたがこのサイトに登録すると、ここで共有された情報を読んだり、書いたり、評価したりして、フォーラムでの情報を集める事ができます。

+ +

このサイトに登録する際には、名前とメールアドレスの入力を求めることがあります。ただし、登録をすることなくこのサイトを利用することも可能です。あなたのメールアドレスは、固有のリンクを含んだメールで確認されます。そのリンクにアクセスした場合にメールアドレスを制御することとなります。

+ +

アカウントを登録し、投稿を行った際にはその投稿が行われたIPアドレスを記録します。また、このサーバーに対する全てのリクエストはIPアドレスを含むサーバーログとして保管されます。

+ +

自分の情報を何に使うのですか?

+ +

このサイトで収集された情報は、次のいくつかの方法で使用されます:

+ +
    +
  • パーソナライズ・エクスペリエンス — あなたの情報は、あなたや他のユーザーのニーズに対応するために役立ちます。
  • +
  • サイトの改善・最適化 — このサービスはあなたから受け取った情報やフィードバックに基づいて提供されるサイトの改善を行いつづけます。
  • +
  • サービスの向上 — あなたの情報は、ユーザーからの要求やサポートへより効果的に対応するために役立ちます。
  • +
  • 定期メールの送信 — メールアドレスは、情報の送信、トピックの変更やユーザー名に関係するお知らせ、お問い合わせに関する返答、その他のリクエストや質問に関してお知らせするために使用されます。
  • +
+ +

自分の情報はどのように保護されるのですか?

+ +

このサービスはあなたの個人情報の入力、送信、またはアクセスに際してあなたの個人情報の安全性を維持するために様々なセキュリティ手段をとっています。

+ +

クッキーを使用していますか?

+ +

はい。クッキーはあなたがウェブブラウザ上で許可した場合にコンピュータのストレージに転送される小さなファイルです。これらのクッキーを使用すると、サイトでブラウザが識別され、登録済みのアカウントを持っている場合は登録済みのアカウントに関連付けがされます。

+ +

クッキーを使用して、今後再度閲覧された場合に前回のデータから設定を呼び出したり、今後の改善のためにサイトのトラフィックやサイトの相互作用に関する集計データを作成します。このサービスは、サイトを訪れた方との理解を深めるために、第三者のサービス提供者と契約することがあります。これらのサービス提供者というものは、このサービスでの業務を行ったり、改善するためにこのサービスの代わって収集された情報を使用することはできません。

+ +

このサイトは外部に何らかの情報を開示していますか?

+ +

私たちは、個人を特定出来る情報を外部へ販売、取引、または他の方法で渡すことはありません。これには、このサイトを操作したり、業務を行ったり、サービスを提供するのに役立つ信頼できる第三者は含まれません。法令遵守、サイトポリシーの施行、このサービスや他の人の権利、財産または安全の保護のために適切であると判断した場合に、あなたの情報を公開する場合があります。ただし、マーケティングや広告、その他の目的で匿名での訪問者情報を他者へ提供することができます。

+ +

サードパーティのリンク

+ +

必要に応じて、このサービスの方針にもとづいてこのサイトや第三者のサービスを提供することがあります。これらの第三者のサイトには、個別の独立したプライバシーポリシーがあります。従って、これらのリンク先のサイトに関するコンテンツや活動にかんしては一切責任を負いません。ですが、サイトの完全性やこれらのサイトに関するフィードバックは非常に重要なものであると認識しております。

+ +

子供のオンライン・プライバシー保護法

+ +

このサイト、製品、サービスはすべて13歳以上の人を対象としております。このサーバーが米国にあり、13歳未満の場合はCOPPA (Children's Online Privacy Protection Act) にもとづいてこのサイトを使用しないでください。

+ +

オンライン限定のプライバシーポリシー

+ +

このオンライン・プライバシーポリシーは、このサイトを通じて収集された情報のみに適用され、オフラインで収集される情報には適用されません。

+ + + +

このサービスを使用することにより、このサイトのプライバシーポリシーに同意するものとします。

+ +

プライバシーポリシーの変更

+ +

プライバシーポリシーを変更する場合は、このページへ変更内容を掲載します。

+ +

この文章のライセンスはCC-BY-SAです。このページは2017年5月6日が最終更新です。

+ +

オリジナルの出典 Discourse privacy policy. + title: "%{instance} 利用規約・プライバシーポリシー" time: formats: default: "%Y年%m月%d日 %H:%M" diff --git a/config/locales/no.yml b/config/locales/no.yml index f71c08c6aff..5fd63f121ed 100644 --- a/config/locales/no.yml +++ b/config/locales/no.yml @@ -321,6 +321,76 @@ click_to_show: Klikk for å vise reblogged: fremhevde sensitive_content: Følsomt innhold + terms: + body_html: | +

Personvernserklæring

+ +

Hvilke opplysninger samler vi?

+ +

Vi samler opplysninger fra deg når du registrerer deg på nettstedet vårt, og vi samler data når du deltar på forumet ved å lese, skrive og evaluere innholdet som deles her.

+ +

Når du registrerer deg på nettstedet vårt, kan du bli bedt om å oppgi navnet og e-postadressen din. Imidlertid kan du besøke nettstedet vårt uten å registrere deg. E-postadressen din vil bli bekreftet med en e-post som inneholder en unik lenke. Hvis siden den lenker til, blir besøkt, vet vi at du har kontroll over e-postadressen.

+ +

Når du registrerer deg og skriver innlegg, registrerer vi IP-adressen som innlegget stammer fra. Vi kan også oppbevare logger som inkluderer IP-adressen til alle forespørslene sendt til tjeneren vår.

+ +

Hva bruker vi opplysningene dine til?

+ +

Alle opplysningene vi samler fra deg, kan bli brukt på en av følgende måter:

+ +
    +
  • For å gjøre opplevelsen din mer personlig. Opplysningene dine hjelper oss å svare bedre på dine individuelle behov.
  • +
  • For å forbedre nettstedet vårt. Vi jobber konstant for å forbedre nettstedets tilbud basert på opplysningene og tilbakemeldingene vi mottar fra deg.
  • +
  • For å forbedre vår kundeservice. Dine opplysninger hjelper oss å svare mer effektivt på dine forespørsler sendt til kundeservice eller behov om støtte.
  • +
  • For å sende periodiske e-poster. E-postadressen du oppgir, kan bli brukt til å sende deg informasjon, påminnelser som du ber om ved endringer av emner eller ved svar til brukernavnet ditt, til henvendelser, og/eller andre forspørsler eller andre spørsmål.
  • +
+ +

Hvordan sikrer vi opplysningene?

+ +

Vi gjennomfører flere sikkerhetstiltak for å holde personopplysningene dine sikre når du skriver inn, lagrer eller henter dem.

+ +

Hva er retningslinjene deres for lagring av data?

+ +

Vi vil forsøke i god tro å:

+ +
    +
  • Ikke oppbevare tjener-logger som inneholder IP-adressen til alle forespørslene til denne tjeneren i lenger enn i 90 dager.
  • +
  • Ikke oppbevare IP-adressene forbundet med registrerte brukere og deres innlegg lenger enn i 5 år.
  • +
+ +

Bruker vi informasjonskapsler?

+ +

Ja. Informasjonskapsler er små filer som et nettsted eller dets tjenesteleverandør overfører til harddisken på datamaskinen din gjennom nettleseren din (dersom du tillater det). Disse informasjonskapslene gjør det mulig for nettstedet å gjenkjenne nettleseren din og, dersom du har en konto, knytte nettleseren til den.

+ +

Vi bruker informasjonskapsler for å forstå og lagre preferansene dine for fremtidige besøk og for å samle aggregatdata om trafikk på og samhandling med nettstedet slik at vi kan tilby bedre opplevelser og verktøy på nettstedet i fremtiden. Vi kan inngå avtaler med tredjeparts tjenesteleverandører for å bistå oss i å forstå besøkerne våres bedre. Disse tjenesteleverandørene har ikke lov til å bruke opplysningene samlet på våres vegne unntatt til å hjelpe oss å gjennomføre og forbedre anliggendet vårt.

+ +

Gir vi noen opplysninger videre til andre parter?

+ +

Vi verken selger, handler med eller overfører på noen annen måte til andre parter dine identifiserbare personopplysninger. Dette inkluderer ikke tredjeparter som har vår tillit og bistår oss i å drive nettstedet, utføre våre anliggender eller yter tjenester til deg, så lenge disse partene samtykker til å behandle disse opplysningene fortrolig. Vi kan også frigi opplysningene dine dersom vi tror at å frigi dem er hensiktsmessig for å overholde loven, håndheve nettstedet retningslinjer eller beskytte våre og andres rettigheter. Imidlertid kan opplysninger som ikke er personlig identifiserbare, bli delt med andre parter for markedsføring, reklame eller annet bruk.

+ +

Tredjeparts lenker

+ +

Av og til, etter skjønn, kan vil inkludere eller tilby tredjeparts produkter eller tjenester på nettstedet vårt. Disse tredjeparts nettstedene har separate og selvstendige personvernerklæringer. Vi bærer derfor intet ansvar eller forpliktelser for innholdet eller aktivitetene til disse nettstedene det lenkes til. Ikke mindre prøver vi å bevare vår eget nettsteds integritet og ønsker enhver tilbakemelding om disse nettstedene velkomne.

+ +

Overensstemmelse med Children's Online Privacy Protection Act

+ +

Nettstedet er rettet mot folk som er minst 13 år gamle. Dersom denne tjeneren er i USA, og du er under 13 år i henhold til kravene i COPPA (Children's Online Privacy Protection Act), ikke bruk dette nettstedet.

+ +

Personvernerklæring bare for nettet

+ +

Denne nett-personvernerklæringen gjelder bare for informasjon samlet gjennom nettstedet vårt og ikke for opplysninger samlet når en er frakoblet.

+ + + +

Ved å bruke dette nettstedet samtykker du til nettstedets personvernerklæring.

+ +

Endringer i vår personvernerklæring

+ +

Dersom vi beslutter å endre personvernerklæringen vår, vil vi publisere disse endringene på denne siden.

+ +

Dette dokumentet er lisensiert under CC-BY-SA. De ble sist oppdatert 12. april 2017.

+ +

Dokumentet er en adoptert og endret versjon fra Discourse privacy policy.

+ title: "%{instance} Personvern og villkår for bruk av nettstedet" time: formats: default: "%-d. %b %Y, %H:%M" diff --git a/config/settings.yml b/config/settings.yml index 7b78b6cdb1c..5aea232e7c7 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -10,6 +10,7 @@ defaults: &defaults site_title: Mastodon site_description: '' site_extended_description: '' + site_terms: '' site_contact_username: '' site_contact_email: '' open_registrations: true From cbe94b88e283554ff27a6ca4ff9a16dbada5e229 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Tue, 4 Jul 2017 22:19:54 +0900 Subject: [PATCH 036/114] Change webpack-dev-server repository (#4061) --- package.json | 2 +- yarn.lock | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 0845e83dbdb..49d9f0fdb4f 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "react-intl-translations-manager": "^5.0.0", "react-test-renderer": "^15.6.1", "sinon": "^2.3.5", - "webpack-dev-server": "lencioni/webpack-dev-server#patch-1", + "webpack-dev-server": "webpack/webpack-dev-server#047a5954398e67b0a17e6be7d5877d9f55f40cba", "yargs": "^8.0.2" }, "optionalDependencies": { diff --git a/yarn.lock b/yarn.lock index a44f4ef7fda..4a909d134a6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3011,10 +3011,6 @@ fs-extra@^0.30.0: path-is-absolute "^1.0.0" rimraf "^2.2.8" -fs-readdir-recursive@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.0.0.tgz#8cd1745c8b4f8a29c8caec392476921ba195f560" - fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -7202,9 +7198,9 @@ webpack-dev-middleware@^1.10.2, webpack-dev-middleware@^1.11.0: path-is-absolute "^1.0.0" range-parser "^1.0.3" -webpack-dev-server@lencioni/webpack-dev-server#patch-1: +webpack-dev-server@webpack/webpack-dev-server#047a5954398e67b0a17e6be7d5877d9f55f40cba: version "2.5.0" - resolved "https://codeload.github.com/lencioni/webpack-dev-server/tar.gz/8978059d9b880c6c28908a6ed4608e27d40f2f69" + resolved "https://codeload.github.com/webpack/webpack-dev-server/tar.gz/047a5954398e67b0a17e6be7d5877d9f55f40cba" dependencies: ansi-html "0.0.7" bonjour "^3.5.0" From 4cddef1cea3b7c44c5c951b2cd6a118edd0332c5 Mon Sep 17 00:00:00 2001 From: Gyuhwan Park Date: Tue, 4 Jul 2017 23:11:23 +0900 Subject: [PATCH 037/114] i18n: Add korean translation (#4064) * Added Korean Translation (based on japanese) * Update korean translation * Update korean translation: fix syntax error * Updated korean translation * Update korean translation * Update ko.json Translate non-translated parts * Update ko.yml Translated missed parts - and fixed some typos * Create simple_form.ko.yml * Updated korean translation * i18n: fix test fails --- app/helpers/settings_helper.rb | 1 + app/javascript/mastodon/locales/ko.json | 176 ++++++++ .../mastodon/locales/whitelist_ko.json | 2 + config/application.rb | 1 + config/locales/ko.yml | 411 ++++++++++++++++++ config/locales/simple_form.ko.yml | 58 +++ 6 files changed, 649 insertions(+) create mode 100644 app/javascript/mastodon/locales/ko.json create mode 100644 app/javascript/mastodon/locales/whitelist_ko.json create mode 100644 config/locales/ko.yml create mode 100644 config/locales/simple_form.ko.yml diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 847eff2e7f4..af950aa634f 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -19,6 +19,7 @@ module SettingsHelper io: 'Ido', it: 'Italiano', ja: '日本語', + ko: '한국어', nl: 'Nederlands', no: 'Norsk', oc: 'Occitan', diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json new file mode 100644 index 00000000000..0a2d76d3a69 --- /dev/null +++ b/app/javascript/mastodon/locales/ko.json @@ -0,0 +1,176 @@ +{ + "account.block": "차단", + "account.block_domain": "{domain} 전체를 숨김", + "account.disclaimer": "이 사용자는 다른 인스턴스에 소속되어 있으므로, 수치가 정확하지 않을 수도 있습니다.", + "account.edit_profile": "프로필 편집", + "account.follow": "팔로우", + "account.followers": "팔로워", + "account.follows": "팔로우", + "account.follows_you": "날 팔로우합니다", + "account.media": "미디어", + "account.mention": "답장", + "account.mute": "뮤트", + "account.posts": "포스트", + "account.report": "신고", + "account.requested": "승인 대기 중", + "account.unblock": "차단 해제", + "account.unblock_domain": "{domain} 숨김 해제", + "account.unfollow": "팔로우 해제", + "account.unmute": "뮤트 해제", + "boost_modal.combo": "다음부터 {combo}를 누르면 이 과정을 건너뛸 수 있습니다.", + "column.blocks": "차단 중인 사용자", + "column.community": "로컬 타임라인", + "column.favourites": "즐겨찾기", + "column.follow_requests": "팔로우 요청", + "column.home": "홈", + "column.mutes": "뮤트 중인 사용자", + "column.notifications": "알림", + "column.public": "연합 타임라인", + "column_back_button.label": "돌아가기", + "column_header.pin": "고정하기", + "column_header.unpin": "고정 해제", + "column_subheading.navigation": "내비게이션", + "column_subheading.settings": "설정", + "compose_form.lock_disclaimer": "이 계정은 {locked}로 설정 되어 있지 않습니다. 누구나 이 계정을 팔로우 할 수 있으며, 팔로워 공개의 포스팅을 볼 수 있습니다.", + "compose_form.lock_disclaimer.lock": "비공개", + "compose_form.placeholder": "지금 무엇을 하고 있나요?", + "compose_form.privacy_disclaimer": "이 계정의 비공개 포스트는 멘션된 사용자가 소속된 {domains}으로 전송됩니다. {domainsCount, plural, one {이 서버를} other {이 서버들을}} 신뢰할 수 있습니까? 포스팅의 프라이버시 보호는 Mastodon 서버에서만 유효합니다. {domains}가 Mastodon 인스턴스가 아닐 경우, 이 투고가 사적인 것으로 취급되지 않은 채 부스트 되거나 원하지 않는 사용자에게 보여질 가능성이 있습니다.", + "compose_form.publish": "Toot", + "compose_form.publish_loud": "{publish}!", + "compose_form.sensitive": "이 미디어를 민감한 미디어로 취급", + "compose_form.spoiler": "텍스트 숨기기", + "compose_form.spoiler_placeholder": "경고", + "confirmation_modal.cancel": "취소", + "confirmations.block.confirm": "차단", + "confirmations.block.message": "정말로 {name}를 차단하시겠습니까?", + "confirmations.delete.confirm": "삭제", + "confirmations.delete.message": "정말로 삭제하시겠습니까?", + "confirmations.domain_block.confirm": "도메인 전체를 숨김", + "confirmations.domain_block.message": "정말로 {domain} 전체를 숨기시겠습니까? 대부분의 경우 개별 차단이나 뮤트로 충분합니다.", + "confirmations.mute.confirm": "뮤트", + "confirmations.mute.message": "정말로 {name}를 뮤트하시겠습니까?", + "emoji_button.activity": "활동", + "emoji_button.flags": "국기", + "emoji_button.food": "음식", + "emoji_button.label": "emoji를 추가", + "emoji_button.nature": "자연", + "emoji_button.objects": "물건", + "emoji_button.people": "사람들", + "emoji_button.search": "검색...", + "emoji_button.symbols": "기호", + "emoji_button.travel": "여행과 장소", + "empty_column.community": "로컬 타임라인에 아무 것도 없습니다. 아무거나 적어 보세요!", + "empty_column.hashtag": "이 해시태그는 아직 사용되지 않았습니다.", + "empty_column.home": "아직 아무도 팔로우 하고 있지 않습니다. {public}를 보러 가거나, 검색하여 다른 사용자를 찾아 보세요.", + "empty_column.home.inactivity": "홈 피드에 아무 것도 없습니다. 한동안 활동하지 않은 경우 곧 원래대로 돌아올 것입니다.", + "empty_column.home.public_timeline": "연합 타임라인", + "empty_column.notifications": "아직 알림이 없습니다. 다른 사람과 대화를 시작해 보세요!", + "empty_column.public": "여기엔 아직 아무 것도 없습니다! 공개적으로 무언가 포스팅하거나, 다른 인스턴스 유저를 팔로우 해서 가득 채워보세요!", + "follow_request.authorize": "허가", + "follow_request.reject": "거부", + "getting_started.appsshort": "어플리케이션", + "getting_started.faq": "자주 있는 질문", + "getting_started.heading": "시작", + "getting_started.open_source_notice": "Mastodon은 오픈 소스 소프트웨어입니다. 누구나 GitHub({github})에서 개발에 참여하거나, 문제를 보고할 수 있습니다.", + "getting_started.userguide": "사용자 가이드", + "home.column_settings.advanced": "고급 사용자용", + "home.column_settings.basic": "기본 설정", + "home.column_settings.filter_regex": "정규 표현식으로 필터링", + "home.column_settings.show_reblogs": "부스트 표시", + "home.column_settings.show_replies": "답글 표시", + "home.settings": "컬럼 설정", + "lightbox.close": "닫기", + "loading_indicator.label": "불러오는 중...", + "media_gallery.toggle_visible": "표시 전환", + "missing_indicator.label": "찾을 수 없습니다", + "navigation_bar.blocks": "차단한 사용자", + "navigation_bar.community_timeline": "로컬 타임라인", + "navigation_bar.edit_profile": "프로필 편집", + "navigation_bar.favourites": "즐겨찾기", + "navigation_bar.follow_requests": "팔로우 요청", + "navigation_bar.info": "이 인스턴스에 대해서", + "navigation_bar.logout": "로그아웃", + "navigation_bar.mutes": "뮤트 중인 사용자", + "navigation_bar.preferences": "사용자 설정", + "navigation_bar.public_timeline": "연합 타임라인", + "notification.favourite": "{name}님이 즐겨찾기 했습니다", + "notification.follow": "{name}님이 나를 팔로우 했습니다", + "notification.mention": "{name}님이 답글을 보냈습니다", + "notification.reblog": "{name}님이 부스트 했습니다", + "notifications.clear": "알림 지우기", + "notifications.clear_confirmation": "정말로 알림을 삭제하시겠습니까?", + "notifications.column_settings.alert": "데스크탑 알림", + "notifications.column_settings.favourite": "즐겨찾기", + "notifications.column_settings.follow": "새 팔로워", + "notifications.column_settings.mention": "답글", + "notifications.column_settings.reblog": "부스트", + "notifications.column_settings.show": "컬럼에 표시", + "notifications.column_settings.sound": "효과음 재생", + "onboarding.done": "완료", + "onboarding.next": "다음", + "onboarding.page_five.public_timelines": "연합 타임라인에서는 {domain}의 사람들이 팔로우 중인 Mastodon 전체 인스턴스의 공개 포스트를 표시합니다. 로컬 타임라인에서는 {domain} 만의 공개 포스트를 표시합니다.", + "onboarding.page_four.home": "홈 타임라인에서는 내가 팔로우 중인 사람들의 포스트를 표시합니다.", + "onboarding.page_four.notifications": "알림에서는 다른 사람들과의 연결을 표시합니다.", + "onboarding.page_one.federation": "Mastodon은 누구나 참가할 수 있는 SNS입니다.", + "onboarding.page_one.handle": "여러분은 지금 수많은 Mastodon 인스턴스 중 하나인 {domain}에 있습니다. あなたのフルハンドルは{handle}です。", + "onboarding.page_one.welcome": "Mastodon에 어서 오세요!", + "onboarding.page_six.admin": "이 인스턴스의 관리자는 {admin}입니다.", + "onboarding.page_six.almost_done": "이상입니다.", + "onboarding.page_six.appetoot": "Bon Appetoot!", + "onboarding.page_six.apps_available": "iOS、Android 또는 다른 플랫폼에서 사용할 수 있는 {apps}이 있습니다.", + "onboarding.page_six.github": "Mastodon는 오픈 소스 소프트웨어입니다. 버그 보고나 기능 추가 요청, 기여는 {github}에서 할 수 있습니다.", + "onboarding.page_six.guidelines": "커뮤니티 가이드라인", + "onboarding.page_six.read_guidelines": "{guidelines}을 확인하는 것을 잊지 마세요.", + "onboarding.page_six.various_app": "다양한 모바일 어플리케이션", + "onboarding.page_three.profile": "[프로필 편집] 에서 자기 소개나 이름을 변경할 수 있습니다. 또한 다른 설정도 변경할 수 있습니다.", + "onboarding.page_three.search": "검색 바에서 {illustration} 나 {introductions} 와 같이 특정 해시태그가 달린 포스트를 보거나, 사용자를 찾을 수 있습니다.", + "onboarding.page_two.compose": "이 폼에서 포스팅 할 수 있습니다. 이미지나 공개 범위 설정, 스포일러 경고 설정은 아래 아이콘으로 설정할 수 있습니다.", + "onboarding.skip": "건너뛰기", + "privacy.change": "포스트의 프라이버시 설정을 변경", + "privacy.direct.long": "멘션한 사용자에게만 공개", + "privacy.direct.short": "다이렉트", + "privacy.private.long": "팔로워에게만 공개", + "privacy.private.short": "비공개", + "privacy.public.long": "공개 타임라인에 표시", + "privacy.public.short": "공개", + "privacy.unlisted.long": "공개 타임라인에 표시하지 않음", + "privacy.unlisted.short": "Unlisted", + "reply_indicator.cancel": "취소", + "report.heading": "신고", + "report.placeholder": "코멘트", + "report.submit": "신고하기", + "report.target": "문제가 된 사용자", + "search.placeholder": "검색", + "search_results.total": "{count, number}건의 결과", + "status.cannot_reblog": "이 포스트는 부스트 할 수 없습니다", + "status.delete": "삭제", + "status.favourite": "즐겨찾기", + "status.load_more": "더 보기", + "status.media_hidden": "미디어 숨겨짐", + "status.mention": "답장", + "status.mute_conversation": "이 대화를 뮤트", + "status.open": "상세 정보 표시", + "status.reblog": "부스트", + "status.reblogged_by": "{name}님이 부스트 했습니다", + "status.reply": "답장", + "status.replyAll": "전원에게 답장", + "status.report": "신고", + "status.sensitive_toggle": "클릭해서 표시하기", + "status.sensitive_warning": "민감한 미디어", + "status.show_less": "숨기기", + "status.show_more": "더 보기", + "status.unmute_conversation": "이 대화의 뮤트 해제하기", + "tabs_bar.compose": "포스트", + "tabs_bar.federated_timeline": "연합", + "tabs_bar.home": "홈", + "tabs_bar.local_timeline": "로컬", + "tabs_bar.notifications": "알림", + "upload_area.title": "드래그 & 드롭으로 업로드", + "upload_button.label": "미디어 추가", + "upload_form.undo": "재시도", + "upload_progress.label": "업로드 중...", + "video_player.expand": "동영상 자세히 보기", + "video_player.toggle_sound": "소리 토글하기", + "video_player.toggle_visible": "표시 전환", + "video_player.video_error": "동영상 재생에 실패했습니다" +} diff --git a/app/javascript/mastodon/locales/whitelist_ko.json b/app/javascript/mastodon/locales/whitelist_ko.json new file mode 100644 index 00000000000..0d4f101c7a3 --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_ko.json @@ -0,0 +1,2 @@ +[ +] diff --git a/config/application.rb b/config/application.rb index 6b121009e10..6bd47cd6c78 100644 --- a/config/application.rb +++ b/config/application.rb @@ -45,6 +45,7 @@ module Mastodon :io, :it, :ja, + :ko, :nl, :no, :oc, diff --git a/config/locales/ko.yml b/config/locales/ko.yml new file mode 100644 index 00000000000..7f238ab739a --- /dev/null +++ b/config/locales/ko.yml @@ -0,0 +1,411 @@ +--- +ko: + about: + about_mastodon: Mastodon 은자유로운 오픈 소스소셜 네트워크입니다. 상용 플랫폼의 대체로써 분산형 구조를 채택해, 여러분의 대화가 한 회사에 독점되는 것을 방지합니다. 신뢰할 수 있는 인스턴스를 선택하세요 — 어떤 인스턴스를 고르더라도, 누구와도 대화할 수 있습니다. 누구나 자신만의 Mastodon 인스턴스를 만들 수 있으며, Seamless하게 소셜 네트워크에 참가할 수 있습니다. + about_this: 이 인스턴스에 대해서 + apps: 어플리케이션 + business_email: '비즈니스 메일 주소:' + closed_registrations: 현재 이 인스턴스에서는 신규 등록을 받고 있지 않습니다. + contact: 연락처 + description_headline: "%{domain} 는 무엇인가요?" + domain_count_after: 개의 인스턴스 + domain_count_before: 연결됨 + features: + api: 어플리케이션이나 그 외 서비스에 API를 공개 + blocks: 강력한 차단 및 뮤트 기능 + characters: 한번에 500자까지 포스팅 가능 + chronology: 시간 순서의 타임라인 + ethics: 광고도 트래킹도 없습니다 + gifv: GIFV나 짧은 동영상도 지원 + privacy: 각 포스팅마다 공개 범위를 상세히 설정 가능 + public: 공개 타임라인 + features_headline: Mastodon 의 특징 + get_started: 참가하기 + links: 링크 + other_instances: 다른 인스턴스 + source_code: 소스 코드 + status_count_after: Toot + status_count_before: Toot 수 + terms: 개인 정보 보호 정책 + user_count_after: 명 + user_count_before: 사용자 수 + version: 버전 + accounts: + follow: 팔로우 + followers: 팔로워 + following: 팔로잉 + nothing_here: 아무 것도 없습니다. + people_followed_by: "%{name} 님이 팔로우 중인 계정" + people_who_follow: "%{name} 님을 팔로우 중인 계정" + posts: 포스트 + remote_follow: 리모트 팔로우 + reserved_username: 이 사용자 명은 예약되어 있습니다. + unfollow: 팔로우 해제 + activitypub: + activity: + announce: + name: "%{account_name} 님이 액티비티를 공유했습니다" + create: + name: "%{account_name} 님이 노트를 작성했습니다" + outbox: + name: "%{account_name} 님의 송신함" + summary: "%{account_name} 님의 액티비티 모음" + admin: + accounts: + are_you_sure: 정말로 실행하시겠습니까? + confirm: 확인 + confirmed: 확인됨 + disable_two_factor_authentication: 2단계 인증을 비활성화 + display_name: 계정명 + domain: 도메인 + edit: 편집 + email: E-mail + feed_url: 피드 URL + followers: 팔로워 수 + follows: 팔로잉 수 + ip: IP + location: + all: 전체 + local: 로컬 + remote: 리모트 + title: 위치 + media_attachments: 첨부된 미디어 + moderation: + all: 전체 + silenced: 침묵 중 + suspended: 정지 중 + title: 모더레이션 + most_recent_activity: 최근 활동 + most_recent_ip: 최근 IP + not_subscribed: 구독하지 않음 + order: + alphabetic: 알파벳 순 + most_recent: 최근 활동 순 + title: 순서 + perform_full_suspension: 완전히 정지시키기 + profile_url: 프로필 URL + public: 전체 공개 + push_subscription_expires: PuSH 구독 기간 만료 + redownload: 아바타 업데이트 + reset: 초기화 + reset_password: 비밀번호 초기화 + resubscribe: 다시 구독 + salmon_url: Salmon URL + search: 검색 + show: + created_reports: 이 계정에서 제출된 신고 + report: 신고 + targeted_reports: 이 계정에 대한 신고 + silence: 침묵 + statuses: Toot 수 + subscribe: 구독하기 + title: 계정 + undo_silenced: 침묵 해제 + undo_suspension: 정지 해제 + unsubscribe: 구독 해제 + username: 사용자명 + web: Web + domain_blocks: + add_new: 추가하기 + created_msg: 도메인 차단 처리를 완료했습니다. + destroyed_msg: 도메인 차단을 해제했습니다. + domain: 도메인 + new: + create: 차단 추가 + hint: 도메인 차단은 내부 데이터베이스에 계정이 생성되는 것까지는 막을 수 없지만, 그 도메인에서 생성된 계정에 자동적으로 특정한 모더레이션을 적용하게 할 수 있습니다. + severity: + desc_html: "침묵은 계정을 팔로우 하지 않고 있는 사람들에겐 계정의 Toot을 보이지 않게 합니다. 정지는 계정의 컨텐츠, 미디어, 프로필 데이터를 삭제합니다." + silence: 침묵 + suspend: 정지 + title: 새로운 도메인 차단 + reject_media: 미디어 파일 거부하기 + reject_media_hint: 로컬에 저장된 미디어 파일을 삭제하고, 이후로도 다운로드를 거부합니다. 정지하고는 관계 없습니다. + severities: + silence: 침묵 + suspend: 정지 + severity: 심각도 + show: + affected_accounts: + one: 데이터베이스 중 1개의 계정에 영향을 끼칩니다 + other: 데이터베이스 중 %{count}개의 계정에 영향을 끼칩니다 + retroactive: + silence: 이 도메인에 존재하는 모든 계정의 침묵를 해제 + suspend: 이 도메인에 존재하는 모든 계정의 계정 정지를 해제 + title: "%{domain}의 도메인 차단을 해제" + undo: 실행 취소 + title: 도메인 차단 + undo: 실행 취소 + instances: + account_count: 알려진 계정의 수 + domain_name: 도메인 이름 + title: 알려진 인스턴스들 + reports: + action_taken_by: 신고 처리자 + are_you_sure: 정말로 실행하시겠습니까? + comment: + label: 코멘트 + none: 없음 + delete: 삭제 + id: ID + mark_as_resolved: 해결 완료 처리 + nsfw: + 'false': NSFW 꺼짐 + 'true': NSFW 켜짐 + report: "신고 #%{id}" + report_contents: 내용 + reported_account: 신고 대상 계정 + reported_by: 신고자 + resolved: 해결됨 + silence_account: 계정을 침묵 처리 + status: 상태 + suspend_account: 계정을 정지 + target: 대상 + title: 신고 + unresolved: 미해결 + view: 표시 + settings: + contact_information: + email: 공개할 메일 주소를 입력 + label: 연락처 정보 + username: 사용자명을 입력 + registrations: + closed_message: + desc_html: 신규 등록을 받지 않을 때 프론트 페이지에 표시됩니다.
HTML 태그를 사용할 수 있습니다. + title: 신규 등록 정지 시 메시지 + open: + disabled: 꺼짐 + enabled: 켜짐 + title: 신규 등록을 받음 + setting: 설정 + site_description: + desc_html: 탑 페이지와 meta 태그에 사용됩니다.
HTML 태그, 예를 들어<a> 태그와 <em> 태그를 사용할 수 있습니다. + title: 사이트 설명 + site_description_extended: + desc_html: 인스턴스 정보 페이지에 표시됩니다.
HTML 태그를 사용할 수 있습니다. + title: 사이트 상세 설명 + site_title: 사이트 이름 + title: 사이트 설정 + subscriptions: + callback_url: 콜백 URL + confirmed: 확인됨 + expires_in: 기한 + last_delivery: 최종 발송 + title: PubSubHubbub + topic: 토픽 + title: 관리 + admin_mailer: + new_report: + body: "%{reporter} 가 %{target} 를 신고했습니다" + subject: "%{instance} 에 새 신고 등록됨 (#%{id})" + application_mailer: + settings: '메일 설정을 변경: %{link}' + signature: Mastodon %{instance} 인스턴스로에서 알림 + view: 'View:' + applications: + invalid_url: 올바르지 않은 URL입니다 + auth: + change_password: 보안 + delete_account: 계정 삭제 + delete_account_html: 계정을 삭제하고 싶은 경우, 여기서 삭제할 수 있습니다. 삭제 전 확인 화면이 표시됩니다. + didnt_get_confirmation: 확인 메일을 받지 못하셨습니까? + forgot_password: 비밀번호를 잊어버리셨습니까? + login: 로그인 + logout: 로그아웃 + register: 등록하기 + resend_confirmation: 확인 메일을 다시 보내기 + reset_password: 비밀번호 재설정 + set_new_password: 새 비밀번호 + authorize_follow: + error: 리모트 팔로우 도중 오류가 발생했습니다. + follow: 팔로우 + prompt_html: '나(%{self}) 는 아래 계정의 팔로우를 요청했습니다:' + title: "%{acct} 를 팔로우" + datetime: + distance_in_words: + about_x_hours: "%{count}시간" + about_x_months: "%{count}월" + about_x_years: "%{count}년" + almost_x_years: "%{count}년" + half_a_minute: 지금 + less_than_x_minutes: "%{count}분" + less_than_x_seconds: 지금 + over_x_years: "%{count}년" + x_days: "%{count}일" + x_minutes: "%{count}분" + x_months: "%{count}월" + x_seconds: "%{count}초" + deletes: + bad_password_msg: 비밀번호가 올바르지 않습니다 + confirm_password: 본인 확인을 위해, 현재 사용 중인 비밀번호를 입력해 주십시오. + description_html: 계정에 업로드된 모든 컨텐츠가 삭제되며, 계정은 비활성화 됩니다. 이것은 영구적으로 이루어지는 것이므로 되돌릴 수 없습니다. 사칭 행위를 방지하기 위해 같은 계정명으로 다시 등록하는 것은 불가능합니다. + proceed: 계정 삭제 + success_msg: 계정이 정상적으로 삭제되었습니다. + warning_html: 삭제가 보장되는 것은 이 인스턴스 상에서의 컨텐츠에 한합니다. 타 인스턴스 등, 외부에 멀리 공유된 컨텐츠는 흔적이 남아 삭제되지 않는 경우도 있습니다. 그리고 현재 접속이 불가능한 서버나, 업데이트를 받지 않게 된 서버에 대해서는 삭제가 반영되지 않을 수도 있습니다. + warning_title: 공유된 컨텐츠에 대해서 + errors: + '403': 이 페이지를 표시할 권한이 없습니다 + '404': 페이지를 찾을 수 없습니다 + '410': 이 페이지는 더 이상 존재하지 않습니다 + '422': + content: 보안 인증에 실패했습니다. Cookie를 차단하고 있진 않습니까? + title: 보안 인증 실패 + '429': 요청 횟수 제한에 도달했습니다. + noscript: Mastodon을 사용하기 위해서는 JavaScript를 켜 주십시오. + exports: + blocks: 차단 + csv: CSV + follows: 팔로우 + mutes: 뮤트 + storage: 미디어 + followers: + domain: 도메인 + explanation_html: 프라이버시를 확보하고 싶은 경우, 누가 여러분을 팔로우 하고 있는지 파악해둘 필요가 있습니다. 프라이빗 포스팅은 여러분의 팔로워가 소속하는 모든 인스턴스로 배달됩니다. 팔로워가 소속된 인스턴스 관리자나 소프트웨어가 여러분의 프라이버시를 존중하고 있는지 잘 모를 경우, 그 팔로워를 삭제하는 것이 좋을 수도 있습니다. + followers_count: 팔로워 수 + lock_link: 비공개 계정 + purge: 팔로워에서 삭제 + success: + one: 1개 도메인에서 팔로워를 soft-block 처리 중... + other: "%{count}개 도메인에서 팔로워를 soft-block 처리 중..." + true_privacy_html: "프라이버시 보호는 End-to-End 암호화로만 이루어 질 수 있다는 것에 유의해 주십시오." + unlocked_warning_html: 누구든 여러분을 팔로우 할 수 있으며, 여러분의 프라이빗 투고를 볼 수 있습니다. 팔로우 할 수 있는 사람을 제한하고 싶은 경우 %{lock_link}에서 설정해 주십시오. + unlocked_warning_title: 이 계정은 비공개로 설정되어 있지 않습니다. + generic: + changes_saved_msg: 정상적으로 변경되었습니다. + powered_by: powered by %{link} + save_changes: 변경 사항을 저장 + validation_errors: + one: 오류가 발생했습니다. 아래 오류를 확인해 주십시오 + other: 오류가 발생했습니다. 아래 %{count}개 오류를 확인해 주십시오 + imports: + preface: 다른 인스턴스에서 내보내기 한 파일에서 팔로우 / 차단 정보를 이 인스턴스 계정으로 불러올 수 있습니다. + success: 파일이 정상적으로 업로드 되었으며, 현재 처리 중입니다. 잠시 후 다시 확인해 주십시오. + types: + blocking: 차단한 계정 목록 + following: 팔로우 중인 계정 목록 + muting: 뮤트 중인 계정 목록 + upload: 업로드 + landing_strip_html: "%{name} 님은 %{link_to_root_path} 인스턴스의 사용자입니다. 계정을 가지고 있다면 팔로우 하거나 대화할 수 있습니다." + landing_strip_signup_html: 아직 계정이 없다면 여기서 등록할 수 있습니다. + media_attachments: + validations: + images_and_video: 이미 사진이 첨부되어 있으므로 동영상을 첨부할 수 없습니다. + too_many: 최대 4개까지 첨부할 수 있습니다. + notification_mailer: + digest: + body: "%{instance} 에서 마지막 로그인 뒤로 일어난 일:" + mention: "%{name} 님이 답장했습니다:" + new_followers_summary: + one: 새 팔로워를 획득했습니다 + other: "%{count} 명의 새 팔로워를 획득했습니다!" + subject: + one: "1건의 새로운 알림 \U0001F418" + other: "%{count}건의 새로운 알림 \U0001F418" + favourite: + body: '%{name} 님이 내 Toot을 즐겨찾기에 등록했습니다.' + subject: "%{name} 님이 내 Toot을 즐겨찾기에 등록했습니다" + follow: + body: "%{name} 님이 나를 팔로우 했습니다" + subject: "%{name} 님이 나를 팔로우 했습니다" + follow_request: + body: "%{name} 님이 내게 팔로우 요청을 보냈습니다." + subject: "%{name} 님으로터의 팔로우 요청" + mention: + body: "%{name} 님이 답장을 보냈습니다:" + subject: "%{name} 님이 답장을 보냈습니다" + reblog: + body: "%{name} 님이 내 Toot을 부스트 했습니다:" + subject: "%{name} 님이 내 Toot을 부스트 했습니다" + pagination: + next: 다음 + prev: 이전 + truncate: "…" + remote_follow: + acct: 사용자명@도메인을 입력해 주십시오 + missing_resource: 리디렉션 대상을 찾을 수 없습니다 + proceed: 팔로우 하기 + prompt: '팔로우 하려 하고 있습니다' + sessions: + activity: 마지막 활동 + browser: 브라우저 + browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + firefox: Firefox + generic: 알 수 없는 브라우저 + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: 현재 세션 + description: "%{browser} on %{platform}" + explanation: 내 Mastodon 계정에 현재 로그인 중인 웹 브라우저 목록입니다. + ip: IP + platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac + other: 알 수 없는 플랫폼 + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + title: 세션 + settings: + authorized_apps: 인증된 어플리케이션 + back: 돌아가기 + delete: 계정 삭제 + edit_profile: 프로필 편집 + export: 데이터 내보내기 + followers: 신뢰 중인 인스턴스 + import: 데이터 가져오기 + preferences: 사용자 설정 + settings: 설정 + two_factor_authentication: 2단계 인증 + statuses: + open_in_web: Web으로 열기 + over_character_limit: 최대 %{max}자까지 입력할 수 있습니다 + show_more: 더 보기 + visibilities: + private: 비공개 + private_long: 팔로워에게만 표시됩니다 + public: 공개 + public_long: 누구나 볼 수 있으며, 공개 타임라인에 표시됩니다 + unlisted: Unlisted + unlisted_long: 누구나 볼 수 있지만, 공개 타임라인에는 표시되지 않습니다 + stream_entries: + click_to_show: 클릭해서 표시 + reblogged: 님이 부스트 했습니다 + sensitive_content: 민감한 컨텐츠 + time: + formats: + default: "%Y년 %m월 %d일 %H:%M" + two_factor_authentication: + code_hint: 확인하기 위해서 인증 어플리케이션에서 표시된 코드를 입력해 주십시오 + description_html: "2단계 인증을 활성화 하면 로그인 시 전화로 인증 코드를 받을 필요가 있습니다." + disable: 비활성화 + enable: 활성화 + enabled: 2단계 인증이 활성화 되어 있습니다 + enabled_success: 2단계 인증이 활성화 되었습니다 + generate_recovery_codes: 복구 코드 생성 + instructions_html: "Google Authenticator, 또는 타 TOTP 어플리케이션에서 이 QR 코드를 스캔해 주십시오. 이후 로그인 시에는 이 어플리케이션에서 생성되는 코드가 필요합니다." + lost_recovery_codes: 복구 코드를 사용하면 휴대전화를 분실한 경우에도 계정에 접근할 수 있게 됩니다. 복구 코드를 분실한 경우에도 여기서 다시 생성할 수 있지만, 예전 복구 코드는 비활성화 됩니다. + manual_instructions: 'QR 코드를 스캔할 수 없어 수동으로 등록을 원하시는 경우 이 비밀 코드를 사용해 주십시오: ' + recovery_codes: 복구 코드 + recovery_codes_regenerated: 복구 코드가 다시 생성되었습니다. + recovery_instructions_html: 휴대전화를 분실한 경우, 아래 복구 코드 중 하나를 사용해 계정에 접근할 수 있습니다. 복구 코드는 안전하게 보관해 주십시오. 이 코드를 인쇄해 중요한 서류와 함께 보관하는 것도 좋습니다. + setup: 초기 설정 + wrong_code: 코드가 올바르지 않습니다. 서버와 휴대전화 간 시간이 일치하는지 확인해 주십시오. + users: + invalid_email: 메일 주소가 올바르지 않습니다 + invalid_otp_token: 2단계 인증 코드가 올바르지 않습니다 diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml new file mode 100644 index 00000000000..85abddcf3fd --- /dev/null +++ b/config/locales/simple_form.ko.yml @@ -0,0 +1,58 @@ +--- +ko: + simple_form: + hints: + defaults: + avatar: PNG, GIF 혹은 JPG. 최대 2MB. 120x120px로 다운스케일 됨 + display_name: + one: 1 글자 남음 + other: %{count} 글자 남음 + header: PNG, GIF 혹은 JPG. 최대 2MB. 700x335px로 다운스케일 됨 + locked: 수동으로 팔로워를 승인하고, 기본 Toot 프라이버시 설정을 팔로워 전용으로 변경 + note: + one: 1 글자 남음 + other: %{count} 글자 남음 + imports: + data: 다른 마스토돈 인스턴스에서 추출된 CSV 파일 + sessions: + otp: 2단계 인증 코드를 휴대전화를 보고 입력하거나, 복구 코드 중 하나를 사용 + user: + filtered_languages: 선택된 언어가 공개 타임라인에서 제외 될 것입니다. + labels: + defaults: + avatar: 아바타 + confirm_new_password: 새로운 비밀번호를 입력 + confirm_password: 현재 비밀번호를 다시 입력 + current_password: 현재 비밀번호를 입력 + data: 데이터 + display_name: 표시되는 이름 + email: 이메일 주소 + header: 헤더 + locale: 언어 + locked: 계정 잠금 + new_password: 새로운 비밀번호 + note: 자기소개 + otp_attempt: 2단계 인증 코드 + password: 비밀번호 + setting_auto_play_gif: 애니메이션 GIF를 자동 재생 + setting_boost_modal: 부스트 전 확인 창을 보여주기 + setting_default_privacy: Toot 프라이버시 + setting_delete_modal: Toot 삭제 전 확인 창을 보여주기 + severity: 심각도 + type: 불러오기 종류 + username: 유저 이름 + interactions: + must_be_follower: 나를 팔로우 하지 않는 사람에게서 온 알림을 차단 + must_be_following: 내가 팔로우 하지 않는 사람에게서 온 알림을 차단 + notification_emails: + digest: 요약 이메일 보내기 + favourite: 누군가 내 상태를 즐겨찾기로 등록했을 때 이메일 보내기 + follow: 누군가 나를 팔로우 했을 때 이메일 보내기 + follow_request: 누군가 나를 팔로우 하길 원할 때 이메일 보내기 + mention: 누군가 나에게 답장했을 때 이메일 보내기 + reblog: 누군가 내 Toot을 부스트 했을 때 이메일 보내기 + 'no': '아니오' + required: + mark: "*" + text: 필수 항목 + 'yes': '네' From 976c18aa5f159d91b26c8aa0d410a519b7b2e3fd Mon Sep 17 00:00:00 2001 From: Minori Hiraoka Date: Tue, 4 Jul 2017 23:48:22 +0900 Subject: [PATCH 038/114] Fix Korean translation (#4065) * Added Korean Translation (based on japanese) * Update korean translation * Update korean translation: fix syntax error * Updated korean translation * Update korean translation * Update ko.json Translate non-translated parts * Update ko.yml Translated missed parts - and fixed some typos * Create simple_form.ko.yml * Update simple_form.ko.yml Translation error fix - password change form * Update simple_form.ko.yml * Update ko.json Missing translation --- app/javascript/mastodon/locales/ko.json | 2 +- config/locales/simple_form.ko.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 0a2d76d3a69..e88d4a53180 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -112,7 +112,7 @@ "onboarding.page_four.home": "홈 타임라인에서는 내가 팔로우 중인 사람들의 포스트를 표시합니다.", "onboarding.page_four.notifications": "알림에서는 다른 사람들과의 연결을 표시합니다.", "onboarding.page_one.federation": "Mastodon은 누구나 참가할 수 있는 SNS입니다.", - "onboarding.page_one.handle": "여러분은 지금 수많은 Mastodon 인스턴스 중 하나인 {domain}에 있습니다. あなたのフルハンドルは{handle}です。", + "onboarding.page_one.handle": "여러분은 지금 수많은 Mastodon 인스턴스 중 하나인 {domain}에 있습니다. 당신의 유저 이름은 {handle} 입니다.", "onboarding.page_one.welcome": "Mastodon에 어서 오세요!", "onboarding.page_six.admin": "이 인스턴스의 관리자는 {admin}입니다.", "onboarding.page_six.almost_done": "이상입니다.", diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml index 85abddcf3fd..b7dbc8bef8f 100644 --- a/config/locales/simple_form.ko.yml +++ b/config/locales/simple_form.ko.yml @@ -21,16 +21,16 @@ ko: labels: defaults: avatar: 아바타 - confirm_new_password: 새로운 비밀번호를 입력 - confirm_password: 현재 비밀번호를 다시 입력 - current_password: 현재 비밀번호를 입력 + confirm_new_password: 새로운 비밀번호 다시 입력 + confirm_password: 현재 비밀번호 다시 입력 + current_password: 현재 비밀번호 입력 data: 데이터 display_name: 표시되는 이름 email: 이메일 주소 header: 헤더 locale: 언어 locked: 계정 잠금 - new_password: 새로운 비밀번호 + new_password: 새로운 비밀번호 입력 note: 자기소개 otp_attempt: 2단계 인증 코드 password: 비밀번호 From 1921ab40ea93c5c082cd53abd6f94da3947a9f73 Mon Sep 17 00:00:00 2001 From: Gyuhwan Park Date: Wed, 5 Jul 2017 00:09:17 +0900 Subject: [PATCH 039/114] i18n: Update korean translation (#4066) * Added Korean Translation (based on japanese) * Update korean translation * Update korean translation: fix syntax error * Updated korean translation * Update korean translation * Update ko.json Translate non-translated parts * Update ko.yml Translated missed parts - and fixed some typos * Create simple_form.ko.yml * Updated korean translation * i18n: fix test fails * Updated korean translation --- config/locales/ko.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 7f238ab739a..844e78908ba 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -39,7 +39,7 @@ ko: people_who_follow: "%{name} 님을 팔로우 중인 계정" posts: 포스트 remote_follow: 리모트 팔로우 - reserved_username: 이 사용자 명은 예약되어 있습니다. + reserved_username: 이 아이디는 예약되어 있습니다. unfollow: 팔로우 해제 activitypub: activity: @@ -56,7 +56,7 @@ ko: confirm: 확인 confirmed: 확인됨 disable_two_factor_authentication: 2단계 인증을 비활성화 - display_name: 계정명 + display_name: 이름 domain: 도메인 edit: 편집 email: E-mail @@ -103,7 +103,7 @@ ko: undo_silenced: 침묵 해제 undo_suspension: 정지 해제 unsubscribe: 구독 해제 - username: 사용자명 + username: 아이디 web: Web domain_blocks: add_new: 추가하기 @@ -167,7 +167,7 @@ ko: contact_information: email: 공개할 메일 주소를 입력 label: 연락처 정보 - username: 사용자명을 입력 + username: 아이디를 입력 registrations: closed_message: desc_html: 신규 등록을 받지 않을 때 프론트 페이지에 표시됩니다.
HTML 태그를 사용할 수 있습니다. @@ -237,7 +237,7 @@ ko: deletes: bad_password_msg: 비밀번호가 올바르지 않습니다 confirm_password: 본인 확인을 위해, 현재 사용 중인 비밀번호를 입력해 주십시오. - description_html: 계정에 업로드된 모든 컨텐츠가 삭제되며, 계정은 비활성화 됩니다. 이것은 영구적으로 이루어지는 것이므로 되돌릴 수 없습니다. 사칭 행위를 방지하기 위해 같은 계정명으로 다시 등록하는 것은 불가능합니다. + description_html: 계정에 업로드된 모든 컨텐츠가 삭제되며, 계정은 비활성화 됩니다. 이것은 영구적으로 이루어지는 것이므로 되돌릴 수 없습니다. 사칭 행위를 방지하기 위해 같은 아이디로 다시 등록하는 것은 불가능합니다. proceed: 계정 삭제 success_msg: 계정이 정상적으로 삭제되었습니다. warning_html: 삭제가 보장되는 것은 이 인스턴스 상에서의 컨텐츠에 한합니다. 타 인스턴스 등, 외부에 멀리 공유된 컨텐츠는 흔적이 남아 삭제되지 않는 경우도 있습니다. 그리고 현재 접속이 불가능한 서버나, 업데이트를 받지 않게 된 서버에 대해서는 삭제가 반영되지 않을 수도 있습니다. @@ -295,8 +295,8 @@ ko: body: "%{instance} 에서 마지막 로그인 뒤로 일어난 일:" mention: "%{name} 님이 답장했습니다:" new_followers_summary: - one: 새 팔로워를 획득했습니다 - other: "%{count} 명의 새 팔로워를 획득했습니다!" + one: 새 팔로워가 생겼습니다! + other: "%{count} 명의 팔로워가 생겼습니다!" subject: one: "1건의 새로운 알림 \U0001F418" other: "%{count}건의 새로운 알림 \U0001F418" @@ -308,7 +308,7 @@ ko: subject: "%{name} 님이 나를 팔로우 했습니다" follow_request: body: "%{name} 님이 내게 팔로우 요청을 보냈습니다." - subject: "%{name} 님으로터의 팔로우 요청" + subject: "%{name} 님이 보낸 팔로우 요청" mention: body: "%{name} 님이 답장을 보냈습니다:" subject: "%{name} 님이 답장을 보냈습니다" @@ -320,7 +320,7 @@ ko: prev: 이전 truncate: "…" remote_follow: - acct: 사용자명@도메인을 입력해 주십시오 + acct: 아이디@도메인을 입력해 주십시오 missing_resource: 리디렉션 대상을 찾을 수 없습니다 proceed: 팔로우 하기 prompt: '팔로우 하려 하고 있습니다' @@ -405,7 +405,7 @@ ko: recovery_codes_regenerated: 복구 코드가 다시 생성되었습니다. recovery_instructions_html: 휴대전화를 분실한 경우, 아래 복구 코드 중 하나를 사용해 계정에 접근할 수 있습니다. 복구 코드는 안전하게 보관해 주십시오. 이 코드를 인쇄해 중요한 서류와 함께 보관하는 것도 좋습니다. setup: 초기 설정 - wrong_code: 코드가 올바르지 않습니다. 서버와 휴대전화 간 시간이 일치하는지 확인해 주십시오. + wrong_code: 코드가 올바르지 않습니다. 서버와 휴대전화 간의 시간이 일치하는지 확인해 주십시오. users: invalid_email: 메일 주소가 올바르지 않습니다 invalid_otp_token: 2단계 인증 코드가 올바르지 않습니다 From a38b34c37a7c87c205f3d0b327649166c9f4f3f8 Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Tue, 4 Jul 2017 23:34:00 +0200 Subject: [PATCH 040/114] i18n: Updated Polish translation (#4068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * i18n: Updated Polish translation Signed-off-by: Marcin Mikołajczak * fuggin nano --- config/locales/pl.yml | 75 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 113d7f235ad..1bf3d972480 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -48,7 +48,7 @@ pl: create: name: "%{account_name} utworzył(a) wpis." outbox: - name: Skrzynka %{account_name} + name: "Skrzynka %{account_name}" summary: Zbiór aktywności użytkownika %{account_name}. admin: accounts: @@ -183,6 +183,9 @@ pl: site_description_extended: desc_html: Wyświetlany w rozszerzonych informacjach o stronie
Możesz korzystać z tagów HTML title: Extended site description + site_terms: + desc_html: Wyświetlana na stronie zasad użytkowania
Możesz używać tagów HTML + title: Polityka prywatności strony site_title: Tytuł strony title: Ustawienia strony subscriptions: @@ -391,6 +394,76 @@ pl: click_to_show: Naciśnij aby wyświetlić reblogged: podbito sensitive_content: Wrażliwa zawartość + terms: + body_html: | +

Polityka prywatności

+ +

Jakie informacje zbieramy?

+ +

Zbieramy informacje podane przy rejestracji i treści utworzone w trakcie korzystania z serwisu.

+ +

Podczas rejestracji, możesz otrzymać prośbę o podanie adresu e-mail. Możesz jednak odwiedzać stronę bez rejestracji. Adres zostanie zweryfikowany przez kliknięcie w link wysłany w wiadomości. Dzięki temu wiemy, że jesteś właścicielem tego adresu.

+ +

Podczas rejestracji i tworzenia postów, Twój adres IP jest zapisywany na naszych serwerach. Możemy też przechowywać adres IP użyty przy każdej operacji w serwisie.

+ +

Jak wykorzystujemy zebrane informacje?

+ +

Zebrane informacje mogą zostać w jednym z następujących celach:

+ +
    +
  • Aby poprawić wrażenia — informacje o Tobie pomagają w dostosowywaniu serwisu do Twoich potrzeb.
  • +
  • Aby usprawnić stronę — nieustannie staramy się ulepszyć stronę na podstawie informacji o Tobie i Twoich opinii.
  • +
  • Aby usprawnić obsługę klienta — informacje pomogą obsłudze klienta utrzymywać kontakt z Tobą.
  • +
  • Aby okazjonalnie wysyłać wiadomości e-mail — Na podany adres e-mail mogą zostać wysłane wiadomości o wspomnieniu o Tobie we wpisach, przejrzeniu Twojego zgłoszenia i innych interakcji z Tobą.
  • +
+ +

Jak zabezpieczamy dane?

+ +

Korzystamy z wielu zabezpieczeń, aby utrudnić osobom niepowołanym dostęp do danych, które wprowadzasz, publikujesz i czytasz.

+ +

Jak długo przechowujecie dane?

+ +

Dołożymy wszelkich starań, aby przechowywać:

+ +
    +
  • dzienniki serwera zawierające adresy IP przypisane do każdych operacji nie dłużej niż 90 dni.
  • +
  • adresy IP przypisane do użytkowników i ich wpisów nie dłużej niż 5 lat.
  • +
+ +

Czy używamy plików cookies?

+ +

Tak. Pliki cookies (zwane często ciasteczkami) są małymi zbiorami danych przechowywanych na Twoim dysku przez stronę internetową, aby rozpoznawać przeglądarkę i powiązać ją (jeżeli jesteś zarejestrowany/a) z Twoim kontem, jeżeli na to pozwolisz.

+ +

Możemy używać ciasteczek, aby skonfigurować stronę na podstawie zapisanych preferencji, oraz dostosować ją do potrzeb innych użytkowników. Możemy korzystać z usług firm trzecich pomagających w zrozumieniu potrzeb użytkownika. Te usługi nie mogą korzystać ze zdobytych danych w celach innych niż analiza pomagająca ulepszać ten serwis.

+ +

Czy przekazujecie dane podmiotów trzecim?

+ +

Nie dokonujemy transakcji danych pozwalających na identyfikację Twojej osoby umieszczonych na tym serwisie. Nie oznacza to, że nie przekazujemy ich zaufanym podmiotom, które korzystają z nich poufnie. Możemy jednak udostępniać dane, jeżeli jest to wymagane prawnie, lub dla utrzymania bezpieczeństwa strony i innych użytkowników. W celach marketingowych (i podobnych) mogą zostać użyte jedynie dane niepozwalające na identyfikację osoby.

+ +

Odnośniki do treści stron trzecich

+ +

Czasem na stronie mogą pojawić się odnośniki do stron trzecich. Mają one odrębne regulaminy i politykę prywatności. Nie odpowiadamy więc za zawartość tych stron. Dokładamy jednak wszelkich starań, aby nie stanowiły one zagrożenia, prosimy jednak o opinie na temat ich wykorzystania.

+ +

Children's Online Privacy Protection Act Compliance

+ +

Ta strona i usługa jest przeznaczona dla osób, które ukończyły 13 lat. Jeżeli serwer znajduje się na terenie USA i nie masz ukończonych 13 lat, zgodnie z amerykańską ustawą COPPA (Children's Online Privacy Protection Act) nie możesz korzystać z tego serwisu.

+ +

Polityka prywatności dotyczy tylko Internetu

+ +

Ta polityka prywatności dotyczy jedynie danych zbieranych w Internecie, nie tych, które przechowywane są na Twoim kompurerze, np. pliki cookies.

+ + + +

Korzystanie ze strony jest równoznaczne z akceptacją naszej polityki prywatności.

+ +

Zmiany w naszej polityce prywatności

+ +

Jeżeli zdecydujemy się na zmiany w polityce prywatności, zmiany pojawią się na tej stronie.

+ +

Dokument jest dostępny na licencji CC-BY-SA. Ostatnio modyfikowany 31 maja 2013, przetłumaczony 4 lipca 2017. Tłumaczenie (mimo dołożenia wszelkich starań) może nie być w pełni poprawne.

+ +

Tekst bazuje na polityce prywatności Discourse. + title: "Zasady korzystania i polityka prywatności %{instance}" time: formats: default: "%b %d, %Y, %H:%M" From bb194ddb3c65394c256c381834ba3bce4d2988cc Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Wed, 5 Jul 2017 21:51:28 +0900 Subject: [PATCH 041/114] Format datetime of subscriptions on admin UI (#4078) --- app/views/admin/subscriptions/_subscription.html.haml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/views/admin/subscriptions/_subscription.html.haml b/app/views/admin/subscriptions/_subscription.html.haml index 024788e135d..1dec8e39623 100644 --- a/app/views/admin/subscriptions/_subscription.html.haml +++ b/app/views/admin/subscriptions/_subscription.html.haml @@ -7,10 +7,12 @@ - if subscription.confirmed? %i.fa.fa-check %td{ style: "color: #{subscription.expired? ? 'red' : 'inherit'};" } - = precede subscription.expired? ? '-' : '' do - = time_ago_in_words(subscription.expires_at) + %time.time-ago{ datetime: subscription.expires_at.iso8601, title: l(subscription.expires_at) } + = precede subscription.expired? ? '-' : '' do + = time_ago_in_words(subscription.expires_at) %td - if subscription.last_successful_delivery_at? - = l subscription.last_successful_delivery_at + %time.formatted{ datetime: subscription.last_successful_delivery_at.iso8601, title: l(subscription.last_successful_delivery_at) } + = l subscription.last_successful_delivery_at - else %i.fa.fa-times From b52a5e6bd60be3f9548cf59eb313b1e6c2f5920e Mon Sep 17 00:00:00 2001 From: unarist Date: Wed, 5 Jul 2017 21:51:53 +0900 Subject: [PATCH 042/114] Show LoadMore button on Notifications even if all items are filtered (#4077) --- app/javascript/mastodon/features/notifications/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index ed4b3ad9822..2f545fa4a32 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -122,7 +122,7 @@ export default class Notifications extends React.PureComponent { let unread = ''; let scrollContainer = ''; - if (!isLoading && notifications.size > 0 && hasMore) { + if (!isLoading && hasMore) { loadMore = ; } @@ -132,7 +132,7 @@ export default class Notifications extends React.PureComponent { if (isLoading && this.scrollableArea) { scrollableArea = this.scrollableArea; - } else if (notifications.size > 0) { + } else if (notifications.size > 0 || hasMore) { scrollableArea = (

{unread} From 5e6acf960183aea9440ce0d9e28c86f043e88c54 Mon Sep 17 00:00:00 2001 From: abcang Date: Wed, 5 Jul 2017 21:54:21 +0900 Subject: [PATCH 043/114] Fix Nokogiri::HTML at FetchLinkCardService (#4072) --- app/services/fetch_link_card_service.rb | 4 +++- spec/fixtures/requests/sjis.txt | 20 +++++++++++++++++++ spec/services/fetch_link_card_service_spec.rb | 10 ++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 spec/fixtures/requests/sjis.txt diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 4ce221267d0..8ddaa2bf403 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +require 'nkf' class FetchLinkCardService < BaseService include HttpHelper @@ -84,7 +85,8 @@ class FetchLinkCardService < BaseService return if response.code != 200 || response.mime_type != 'text/html' - page = Nokogiri::HTML(response.to_s) + html = response.to_s + page = Nokogiri::HTML(html, nil, NKF.guess(html).to_s) card.type = :link card.title = meta_property(page, 'og:title') || page.at_xpath('//title')&.content diff --git a/spec/fixtures/requests/sjis.txt b/spec/fixtures/requests/sjis.txt new file mode 100644 index 00000000000..9041aa25d6e --- /dev/null +++ b/spec/fixtures/requests/sjis.txt @@ -0,0 +1,20 @@ +HTTP/1.1 200 OK +Server: nginx/1.11.10 +Date: Tue, 04 Jul 2017 16:43:39 GMT +Content-Type: text/html +Content-Length: 273 +Connection: keep-alive +Last-Modified: Tue, 04 Jul 2017 16:41:34 GMT +Accept-Ranges: bytes + + + + + + JSIS̃y[W + + +

SJIS̃y[W
+

+ + diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb index 9df41cf559d..7d7f8e74847 100644 --- a/spec/services/fetch_link_card_service_spec.rb +++ b/spec/services/fetch_link_card_service_spec.rb @@ -6,6 +6,8 @@ RSpec.describe FetchLinkCardService do before do stub_request(:head, 'http://example.xn--fiqs8s/').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.xn--fiqs8s/').to_return(request_fixture('idn.txt')) + stub_request(:head, 'http://example.com/sjis').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) + stub_request(:get, 'http://example.com/sjis').to_return(request_fixture('sjis.txt')) stub_request(:head, 'https://github.com/qbi/WannaCry').to_return(status: 404) subject.call(status) @@ -19,6 +21,14 @@ RSpec.describe FetchLinkCardService do expect(a_request(:get, 'http://example.xn--fiqs8s/')).to have_been_made.at_least_once end end + + context do + let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis') } + + it 'works with SJIS' do + expect(a_request(:get, 'http://example.com/sjis')).to have_been_made.at_least_once + end + end end context 'in a remote status' do From a37cf9548c2772f5b8b7a976e4f90c0ed78a8722 Mon Sep 17 00:00:00 2001 From: "Akihiko Odaki (@fn_aki@pawoo.net)" Date: Thu, 6 Jul 2017 06:58:03 +0900 Subject: [PATCH 044/114] Explicitly require MIME::Types (#4083) --- Gemfile | 1 + Gemfile.lock | 1 + app/models/media_attachment.rb | 2 ++ 3 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index aecd82702d5..6ee884a17a4 100644 --- a/Gemfile +++ b/Gemfile @@ -35,6 +35,7 @@ gem 'http_accept_language', '~> 2.1' gem 'httplog', '~> 0.99' gem 'kaminari', '~> 1.0' gem 'link_header', '~> 0.0' +gem 'mime-types', '~> 3.1' gem 'nokogiri', '~> 1.7' gem 'oj', '~> 3.0' gem 'ostatus2', '~> 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index dac6169d574..f0156529ca5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -516,6 +516,7 @@ DEPENDENCIES link_header (~> 0.0) lograge (~> 0.5) microformats2 (~> 3.0) + mime-types (~> 3.1) nokogiri (~> 1.7) oj (~> 3.0) ostatus2 (~> 2.0) diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 340109ab607..1e8c6d00ae3 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -18,6 +18,8 @@ # file_meta :json # +require 'mime/types' + class MediaAttachment < ApplicationRecord self.inheritance_column = nil From 6d106d3943f47cc48fb4799712bc6dd673b2506b Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Thu, 6 Jul 2017 15:25:27 +0200 Subject: [PATCH 045/114] i18n: minor changes in Polish translation (#4087) * i18n: minor changes in Polish translation * Update pl.json --- app/javascript/mastodon/locales/pl.json | 2 +- config/locales/pl.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index bf425501fa9..2a69824ee2e 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -6,7 +6,7 @@ "account.follow": "Obserwuj", "account.followers": "Obserwujący", "account.follows": "Obserwacje", - "account.follows_you": "Obserwują cię", + "account.follows_you": "Obserwuje cię", "account.media": "Media", "account.mention": "Wspomnij o @{name}", "account.mute": "Wycisz @{name}", diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 1bf3d972480..f0546dd0ce6 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -209,9 +209,9 @@ pl: auth: change_password: Bezpieczeństwo delete_account: Usunięcie konta - delete_account_html: Jeżeli próbowałeś usunąć konto, przejdź tutaj. Otrzymasz prośbę o potwierdzenie. + delete_account_html: Jeżeli chcesz usunąć konto, przejdź tutaj. Otrzymasz prośbę o potwierdzenie. didnt_get_confirmation: Nie otrzymałeś instrukcji weryfikacji? - forgot_password: Zapomniane hasło + forgot_password: Nie pamiętasz hasła? login: Zaloguj się logout: Wyloguj się register: Rejestracja From e7c0d87d984108a5c56c08285e8496a23fca28b8 Mon Sep 17 00:00:00 2001 From: Shin Kojima Date: Thu, 6 Jul 2017 22:27:02 +0900 Subject: [PATCH 046/114] Fix embedded SVG fill attribute (#4086) * Fix embedded SVG fill attribute SCSS darken/lighten functions may not return a color value, but a color name like "white". See following example: https://www.sassmeister.com/gist/c41da93b87d536890ddf30a1f42e7816 This patch will normalize $color argument to FFFFFF style. I also changed the function name from "url-friendly-colour" to "hex-color", Because... 1. The name "url-friendly" is not meaningful enough to describe what it does. 2. It is familier to me using "color" rather than "colour" kojima:kojiMac mastodon[master]$ git grep -l colour app/javascript/styles/boost.scss spec/fixtures/files/attachment.jpg kojima:kojiMac mastodon[master]$ git grep -l color .rspec .scss-lint.yml Gemfile.lock app/javascript/mastodon/features/status/components/action_bar.js app/javascript/styles/about.scss app/javascript/styles/accounts.scss app/javascript/styles/admin.scss app/javascript/styles/basics.scss app/javascript/styles/boost.scss app/javascript/styles/compact_header.scss app/javascript/styles/components.scss app/javascript/styles/containers.scss app/javascript/styles/footer.scss app/javascript/styles/forms.scss app/javascript/styles/landing_strip.scss app/javascript/styles/reset.scss app/javascript/styles/stream_entries.scss app/javascript/styles/tables.scss app/javascript/styles/variables.scss app/views/admin/subscriptions/_subscription.html.haml app/views/layouts/application.html.haml app/views/layouts/error.html.haml app/views/manifests/show.json.rabl bin/webpack-dev-server config/initializers/httplog.rb public/500.html public/emoji/1f1e6-1f1e8.svg public/emoji/1f1ec-1f1f8.svg public/emoji/1f1f3-1f1ee.svg public/emoji/1f1fb-1f1ec.svg spec/fixtures/requests/idn.txt yarn.lock * Add semicolon --- app/javascript/styles/boost.scss | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/javascript/styles/boost.scss b/app/javascript/styles/boost.scss index 7589828c670..8d6478e1074 100644 --- a/app/javascript/styles/boost.scss +++ b/app/javascript/styles/boost.scss @@ -1,11 +1,14 @@ -@function url-friendly-colour($colour) { - @return '%23' + str-slice('#{$colour}', 2, -1) +@function hex-color($color) { + @if type-of($color) == 'color' { + $color: str-slice(ie-hex-str($color), 4); + } + @return '%23' + unquote($color) } button.icon-button i.fa-retweet { - background-image: url("data:image/svg+xml;utf8,"); + background-image: url("data:image/svg+xml;utf8,"); &:hover { - background-image: url("data:image/svg+xml;utf8,"); + background-image: url("data:image/svg+xml;utf8,"); } } From 26949607d25872d4549f96ecf38c317c2774c225 Mon Sep 17 00:00:00 2001 From: Quent-in Date: Thu, 6 Jul 2017 21:10:12 +0200 Subject: [PATCH 047/114] l10n Occitan locale (#4089) * Small adjustments About the report part. * Update time format --- app/javascript/mastodon/locales/oc.json | 10 +++++----- config/locales/oc.yml | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index a2a82ae9ffb..5f226bc70a2 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -34,7 +34,7 @@ "compose_form.lock_disclaimer": "Vòstre compte es pas {locked}. Tot lo mond pòt vos sègre e veire los estatuts reservats als seguidors.", "compose_form.lock_disclaimer.lock": "clavat", "compose_form.placeholder": "A de qué pensatz ?", - "compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz d’aqueste{domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias a Mastodons. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas d’indicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists", + "compose_form.privacy_disclaimer": "Vòstre estatut privat serà enviat a las personas mencionadas sus {domains}. Vos fisatz d’aqueste {domainsCount, plural, one { servidor} other {s servidors}} per divulgar pas vòstre estatut ? Los estatuts privats foncionan pas que sus las instàncias de Mastodon. Se {domains} {domainsCount, plural, one {es pas una instància a Mastodon} other {son pas d'instàncias a Mastodon}}, i aurà pas d’indicacion disent que vòstre estatut es privat e poirà èsser partejat o èsser visible a de mond pas prevists", "compose_form.publish": "Tut", "compose_form.publish_loud": "{publish} !", "compose_form.sensitive": "Marcar lo mèdia coma sensible", @@ -51,7 +51,7 @@ "confirmations.mute.message": "Sètz segur de voler metre en silenci {name} ?", "emoji_button.activity": "Activitat", "emoji_button.flags": "Drapèus", - "emoji_button.food": "Manjar e beure", + "emoji_button.food": "Beure e manjar", "emoji_button.label": "Inserir un emoji", "emoji_button.nature": "Natura", "emoji_button.objects": "Objèctes", @@ -136,10 +136,10 @@ "privacy.unlisted.long": "Mostrar pas dins los fluxes publics", "privacy.unlisted.short": "Pas-listat", "reply_indicator.cancel": "Anullar", - "report.heading": "Nòu senhalament", + "report.heading": "Senhalar {target}", "report.placeholder": "Comentaris addicionals", - "report.submit": "Mandat", - "report.target": "Senhalament", + "report.submit": "Mandar", + "report.target": "Senhalar {target}", "search.placeholder": "Recercar", "search_results.total": "{count, number} {count, plural, one {resultat} other {resultats}}", "status.cannot_reblog": "Aqueste estatut pòt pas èsser partejat", diff --git a/config/locales/oc.yml b/config/locales/oc.yml index c882b43a111..631133f745d 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -242,9 +242,9 @@ oc: - divendres - dissabte formats: - default: "%d/%m/%Y" - long: Lo %B %d de %Y - short: "%b %d" + default: "%e/%m/%Y" + long: Lo %e %B de %Y + short: "%e %b" month_names: - - de genièr From 34c8a46d7de2b7e0b495b5c4a51c95c6a6b047b7 Mon Sep 17 00:00:00 2001 From: Mantas Date: Thu, 6 Jul 2017 22:26:07 +0300 Subject: [PATCH 048/114] Remove ugly blue highlight on Android browsers (#4031) --- app/javascript/styles/basics.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss index 70a5be36785..0cb271ddde6 100644 --- a/app/javascript/styles/basics.scss +++ b/app/javascript/styles/basics.scss @@ -11,6 +11,8 @@ body { text-rendering: optimizelegibility; font-feature-settings: "kern"; text-size-adjust: none; + -webkit-tap-highlight-color: rgba(0,0,0,0); + -webkit-tap-highlight-color: transparent; &.app-body { position: fixed; From 9c03fd9caef575792f08fd4e5c396d8d72bad09f Mon Sep 17 00:00:00 2001 From: unarist Date: Fri, 7 Jul 2017 04:26:21 +0900 Subject: [PATCH 049/114] Unobserve status on unmount (#4013) This fixes a warning on status unmounting (e.g. deletion). This also resets IntersectionObserverWrapper on disconnect to avoid `unobserve()` calls which has bug in Edge. --- app/javascript/mastodon/components/status.js | 4 ++++ .../features/ui/util/intersection_observer_wrapper.js | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index a837659c2cd..ff574ab3d3b 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -90,6 +90,10 @@ export default class Status extends ImmutablePureComponent { } componentWillUnmount () { + if (this.props.intersectionObserverWrapper) { + this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node); + } + this.componentMounted = false; } diff --git a/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js b/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js index 0e959f9ae8e..2b24c65831d 100644 --- a/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js +++ b/app/javascript/mastodon/features/ui/util/intersection_observer_wrapper.js @@ -37,9 +37,18 @@ class IntersectionObserverWrapper { } } + unobserve (id, node) { + if (this.observer) { + delete this.callbacks[id]; + this.observer.unobserve(node); + } + } + disconnect () { if (this.observer) { + this.callbacks = {}; this.observer.disconnect(); + this.observer = null; } } From 6bf6d35637abc691cf85b1c96a54c74af8b8bc2e Mon Sep 17 00:00:00 2001 From: STJrInuyasha Date: Thu, 6 Jul 2017 12:30:37 -0700 Subject: [PATCH 050/114] Parse links in status content on update as well as mount (#4042) * Update links in status content on update as well as mount Fixes occasional bugs with mentions and hashtags not being set to open in a new column like they should, and instead opening in a new page * use classList instead of raw className --- .../mastodon/components/status_content.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 19bde01bd36..78656571d11 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -25,12 +25,17 @@ export default class StatusContent extends React.PureComponent { hidden: true, }; - componentDidMount () { + _updateStatusLinks () { const node = this.node; const links = node.querySelectorAll('a'); for (var i = 0; i < links.length; ++i) { - let link = links[i]; + let link = links[i]; + if (link.classList.contains('status-link')) { + continue; + } + link.classList.add('status-link'); + let mention = this.props.status.get('mentions').find(item => link.href === item.get('url')); if (mention) { @@ -46,10 +51,15 @@ export default class StatusContent extends React.PureComponent { } } + componentDidMount () { + this._updateStatusLinks(); + } + componentDidUpdate () { if (this.props.onHeightUpdate) { this.props.onHeightUpdate(); } + this._updateStatusLinks(); } onMentionClick = (mention, e) => { From f76e71825da3b185e549cd3267813fd12cee4050 Mon Sep 17 00:00:00 2001 From: abcang Date: Fri, 7 Jul 2017 04:31:03 +0900 Subject: [PATCH 051/114] Improve Activity stream spoiler (#4088) --- app/javascript/packs/public.js | 8 ++++++-- app/javascript/styles/stream_entries.scss | 12 ++++++++++++ app/views/stream_entries/_content_spoiler.html.haml | 10 +++++++--- app/views/stream_entries/_detailed_status.html.haml | 6 ++---- app/views/stream_entries/_simple_status.html.haml | 3 +-- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index a0e511b0a3f..254250a3b95 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -53,8 +53,12 @@ function main() { } }); - delegate(document, '.media-spoiler', 'click', ({ target }) => { - target.style.display = 'none'; + delegate(document, '.activity-stream .media-spoiler-wrapper .media-spoiler', 'click', function() { + this.parentNode.classList.add('media-spoiler-wrapper__visible'); + }); + + delegate(document, '.activity-stream .media-spoiler-wrapper .spoiler-button', 'click', function() { + this.parentNode.classList.remove('media-spoiler-wrapper__visible'); }); delegate(document, '.webapp-btn', 'click', ({ target, button }) => { diff --git a/app/javascript/styles/stream_entries.scss b/app/javascript/styles/stream_entries.scss index fcec32d4473..e89cc3f0901 100644 --- a/app/javascript/styles/stream_entries.scss +++ b/app/javascript/styles/stream_entries.scss @@ -330,6 +330,18 @@ } } + .media-spoiler-wrapper { + &.media-spoiler-wrapper__visible { + .media-spoiler { + display: none; + } + + .spoiler-button { + display: block; + } + } + } + .pre-header { padding: 14px 0; padding-left: (48px + 14px * 2); diff --git a/app/views/stream_entries/_content_spoiler.html.haml b/app/views/stream_entries/_content_spoiler.html.haml index d80ea46a03e..798dfce670d 100644 --- a/app/views/stream_entries/_content_spoiler.html.haml +++ b/app/views/stream_entries/_content_spoiler.html.haml @@ -1,3 +1,7 @@ -.media-spoiler - %span= t('stream_entries.sensitive_content') - %span= t('stream_entries.click_to_show') +.media-spoiler-wrapper{ class: sensitive == false && 'media-spoiler-wrapper__visible' } + .spoiler-button + .icon-button.overlayed + %i.fa.fa-fw.fa-eye + .media-spoiler + %span= t('stream_entries.sensitive_content') + %span= t('stream_entries.click_to_show') diff --git a/app/views/stream_entries/_detailed_status.html.haml b/app/views/stream_entries/_detailed_status.html.haml index ef60b9925a3..193cc64702e 100644 --- a/app/views/stream_entries/_detailed_status.html.haml +++ b/app/views/stream_entries/_detailed_status.html.haml @@ -17,13 +17,11 @@ - unless status.media_attachments.empty? - if status.media_attachments.first.video? .video-player - - if status.sensitive? - = render partial: 'stream_entries/content_spoiler' + = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? } %video.u-video{ src: status.media_attachments.first.file.url(:original), loop: true } - else .detailed-status__attachments - - if status.sensitive? - = render partial: 'stream_entries/content_spoiler' + = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? } .status__attachments__inner - status.media_attachments.each do |media| = render partial: 'stream_entries/media', locals: { media: media } diff --git a/app/views/stream_entries/_simple_status.html.haml b/app/views/stream_entries/_simple_status.html.haml index db4e30fda47..2df0cc850a4 100644 --- a/app/views/stream_entries/_simple_status.html.haml +++ b/app/views/stream_entries/_simple_status.html.haml @@ -22,8 +22,7 @@ - unless status.media_attachments.empty? .status__attachments - - if status.sensitive? - = render partial: 'stream_entries/content_spoiler' + = render partial: 'stream_entries/content_spoiler', locals: { sensitive: status.sensitive? } - if status.media_attachments.first.video? .status__attachments__inner .video-item From 18d3fa953b5af8ab17cc93c33cb95cec37127712 Mon Sep 17 00:00:00 2001 From: Damien Erambert Date: Thu, 6 Jul 2017 13:39:56 -0700 Subject: [PATCH 052/114] Add a setting allowing the use of system's default font in Web UI (#4033) * add a system_font_ui setting on the server * Plug the system_font_ui on the front-end * add EN/FR locales for the new setting * put Roboto after all other fonts * remove trailing whitespace so CodeClimate is happy * fix user_spec.rb * correctly write user_spect this time * slightly better way of adding the classes * add comments to the system-font stack for clarification * use .system-font for the class instead * don't use multiple lines for comments * remove trailing whitespace * use the classnames module for consistency * use `mastodon-font-sans-serif` instead of Roboto directly --- .../settings/preferences_controller.rb | 1 + app/javascript/mastodon/features/ui/index.js | 14 ++++++++++++-- app/javascript/styles/basics.scss | 15 +++++++++++++++ app/lib/user_settings_decorator.rb | 5 +++++ app/models/user.rb | 4 ++++ app/views/home/initial_state.json.rabl | 1 + app/views/settings/preferences/show.html.haml | 1 + config/locales/simple_form.en.yml | 1 + config/locales/simple_form.fr.yml | 1 + config/settings.yml | 1 + spec/lib/user_settings_decorator_spec.rb | 7 +++++++ spec/models/user_spec.rb | 8 ++++++++ 12 files changed, 57 insertions(+), 2 deletions(-) diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index 71f5a7c04be..a15c26031bf 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -37,6 +37,7 @@ class Settings::PreferencesController < ApplicationController :setting_boost_modal, :setting_delete_modal, :setting_auto_play_gif, + :setting_system_font_ui, notification_emails: %i(follow follow_request reblog favourite mention digest), interactions: %i(must_be_follower must_be_following) ) diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 08d087da1b8..54e623d9952 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -1,4 +1,5 @@ import React from 'react'; +import classNames from 'classnames'; import Switch from 'react-router-dom/Switch'; import Route from 'react-router-dom/Route'; import Redirect from 'react-router-dom/Redirect'; @@ -72,12 +73,17 @@ class WrappedRoute extends React.Component { } -@connect() +const mapStateToProps = state => ({ + systemFontUi: state.getIn(['meta', 'system_font_ui']), +}); + +@connect(mapStateToProps) export default class UI extends React.PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, children: PropTypes.node, + systemFontUi: PropTypes.bool, }; state = { @@ -176,8 +182,12 @@ export default class UI extends React.PureComponent { const { width, draggingOver } = this.state; const { children } = this.props; + const className = classNames('ui', { + 'system-font': this.props.systemFontUi, + }); + return ( -
+
diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss index 0cb271ddde6..4da698e8169 100644 --- a/app/javascript/styles/basics.scss +++ b/app/javascript/styles/basics.scss @@ -63,3 +63,18 @@ button { align-items: center; justify-content: center; } + +.system-font { + // system-ui => standard property (Chrome/Android WebView 56+, Opera 43+, Safari 11+) + // -apple-system => Safari <11 specific + // BlinkMacSystemFont => Chrome <56 on macOS specific + // Segoe UI => Windows 7/8/10 + // Oxygen => KDE + // Ubuntu => Unity/Ubuntu + // Cantarell => GNOME + // Fira Sans => Firefox OS + // Droid Sans => Older Androids (<4.0) + // Helvetica Neue => Older macOS <10.11 + // mastodon-font-sans-serif => web-font (Roboto) fallback and newer Androids (>=4.0) + font-family: system-ui, -apple-system,BlinkMacSystemFont, "Segoe UI","Oxygen", "Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",mastodon-font-sans-serif, sans-serif; +} diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index af264bbd535..9c0cb454597 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -21,6 +21,7 @@ class UserSettingsDecorator user.settings['boost_modal'] = boost_modal_preference user.settings['delete_modal'] = delete_modal_preference user.settings['auto_play_gif'] = auto_play_gif_preference + user.settings['system_font_ui'] = system_font_ui_preference end def merged_notification_emails @@ -43,6 +44,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_delete_modal' end + def system_font_ui_preference + boolean_cast_setting 'setting_system_font_ui' + end + def auto_play_gif_preference boolean_cast_setting 'setting_auto_play_gif' end diff --git a/app/models/user.rb b/app/models/user.rb index c31a0c64404..e2bb3d0ed20 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -91,6 +91,10 @@ class User < ApplicationRecord settings.auto_play_gif end + def setting_system_font_ui + settings.system_font_ui + end + def activate_session(request) session_activations.activate(session_id: SecureRandom.hex, user_agent: request.user_agent, diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl index e305f8e7ae2..291ff806ba0 100644 --- a/app/views/home/initial_state.json.rabl +++ b/app/views/home/initial_state.json.rabl @@ -11,6 +11,7 @@ node(:meta) do boost_modal: current_account.user.setting_boost_modal, delete_modal: current_account.user.setting_delete_modal, auto_play_gif: current_account.user.setting_auto_play_gif, + system_font_ui: current_account.user.setting_system_font_ui, } end diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 721ce6a219a..26fbfdf82ab 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -44,6 +44,7 @@ .fields-group = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label + = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 3e769fb96be..d8d3b8a6fb7 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -38,6 +38,7 @@ en: setting_boost_modal: Show confirmation dialog before boosting setting_default_privacy: Post privacy setting_delete_modal: Show confirmation dialog before deleting a toot + setting_system_font_ui: Use system's default font severity: Severity type: Import type username: Username diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index ae4975143c8..446c569473e 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -28,6 +28,7 @@ fr: password: Mot de passe setting_boost_modal: Afficher un dialogue de confirmation avant de partager setting_default_privacy: Confidentialité des statuts + setting_system_font_ui: Utiliser la police par défaut du système severity: Séverité type: Type d'import username: Identifiant diff --git a/config/settings.yml b/config/settings.yml index 5aea232e7c7..18b70b51f06 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -19,6 +19,7 @@ defaults: &defaults boost_modal: false auto_play_gif: true delete_modal: true + system_font_ui: false notification_emails: follow: false reblog: false diff --git a/spec/lib/user_settings_decorator_spec.rb b/spec/lib/user_settings_decorator_spec.rb index 66e42fa0ecc..e1ba56d9754 100644 --- a/spec/lib/user_settings_decorator_spec.rb +++ b/spec/lib/user_settings_decorator_spec.rb @@ -48,5 +48,12 @@ describe UserSettingsDecorator do settings.update(values) expect(user.settings['auto_play_gif']).to eq false end + + it 'updates the user settings value for system font in UI' do + values = { 'setting_system_font_ui' => '0' } + + settings.update(values) + expect(user.settings['system_font_ui']).to eq false + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a6df3fb2621..2019ec0f698 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -184,6 +184,14 @@ RSpec.describe User, type: :model do expect(user.setting_auto_play_gif).to eq false end end + + describe '#setting_system_font_ui' do + it 'returns system font ui setting' do + user = Fabricate(:user) + user.settings[:system_font_ui] = false + expect(user.setting_system_font_ui).to eq false + end + end describe '#setting_boost_modal' do it 'returns boost modal setting' do From 2083000027451249836c369eb961300d7faf5f99 Mon Sep 17 00:00:00 2001 From: "Akihiko Odaki (@fn_aki@pawoo.net)" Date: Fri, 7 Jul 2017 07:12:12 +0900 Subject: [PATCH 053/114] Set default From address in config (#3756) The old implementation sets default From address in mailers. It sets the address from SMTP_FROM_ADDRESS, or notifications@localhost. The behavior is occasionally undesired results. In production environment, notifications@localhost is likely to be incorrect. In testing environment, the email address should not be varied by a environment variable. After appling this change, In production environment, it will throw an exception when launching Mastodon. In testing environment, the address will be fixed with notifications@localhost. --- app/mailers/application_mailer.rb | 1 - app/mailers/user_mailer.rb | 1 - config/environments/development.rb | 2 ++ config/environments/production.rb | 2 ++ config/environments/test.rb | 2 ++ 5 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 2e730c19b04..95b770ff111 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ApplicationMailer < ActionMailer::Base - default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' } layout 'mailer' helper :instance diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 6abf9c9caff..1517c027e87 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class UserMailer < Devise::Mailer - default from: ENV.fetch('SMTP_FROM_ADDRESS') { 'notifications@localhost' } layout 'mailer' helper :instance diff --git a/config/environments/development.rb b/config/environments/development.rb index c81cf7bbedd..406fa970b2d 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -62,6 +62,8 @@ Rails.application.configure do # routes, locales, etc. This feature depends on the listen gem. # config.file_watcher = ActiveSupport::EventedFileUpdateChecker + config.action_mailer.default_options = { from: 'notifications@localhost' } + # If using a Heroku, Vagrant or generic remote development environment, # use letter_opener_web, accessible at /letter_opener. # Otherwise, use letter_opener, which launches a browser window to view sent mail. diff --git a/config/environments/production.rb b/config/environments/production.rb index bd87d79a785..d9c9da684fd 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -73,6 +73,8 @@ Rails.application.configure do config.action_mailer.perform_caching = false # E-mails + config.action_mailer.default_options = { from: ENV.fetch('SMTP_FROM_ADDRESS') } + config.action_mailer.smtp_settings = { :port => ENV['SMTP_PORT'], :address => ENV['SMTP_SERVER'], diff --git a/config/environments/test.rb b/config/environments/test.rb index db98263a64a..bde69eba15a 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -30,6 +30,8 @@ Rails.application.configure do config.action_controller.allow_forgery_protection = false config.action_mailer.perform_caching = false + config.action_mailer.default_options = { from: 'notifications@localhost' } + # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. From 185b41beb4adea5f0b55f56c0898bef74fd35435 Mon Sep 17 00:00:00 2001 From: Daniel Hunsaker Date: Thu, 6 Jul 2017 16:46:45 -0600 Subject: [PATCH 054/114] [nanobox] Add Automated Backups (#4023) This PR adds automatic backups to Nanobox instances. The database, Redis, and user files are backed up every day at 03:00 (server time) to the data warehouse component which comes with every Nanobox app. Old backups are automatically cleared out, but the number of backups that are left untouched can be configured by setting the `BACKUP_COUNT` environment variable to any integer value greater than 0 (the default is 1). Also updated `.env.nanobox` to reflect the current `.env.production.sample`. --- .env.nanobox | 2 +- boxfile.yml | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/.env.nanobox b/.env.nanobox index 73abefdc657..7920c47b95d 100644 --- a/.env.nanobox +++ b/.env.nanobox @@ -69,7 +69,7 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io # PAPERCLIP_ROOT_URL=/system # Optional asset host for multi-server setups -# CDN_HOST=assets.example.com +# CDN_HOST=https://assets.example.com # S3 (optional) # S3_ENABLED=true diff --git a/boxfile.yml b/boxfile.yml index ef847d4a036..3302231109e 100644 --- a/boxfile.yml +++ b/boxfile.yml @@ -153,8 +153,59 @@ worker.sidekiq: data.db: image: nanobox/postgresql:9.5 + cron: + - id: backup + schedule: '0 3 * * *' + command: | + PGPASSWORD=${DATA_POSTGRES_PASS} pg_dump -U ${DATA_POSTGRES_USER} -w -Fc -O gonano | + gzip | + curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/backup-${HOSTNAME}-$(date -u +%Y-%m-%d.%H-%M-%S).sql.gz --data-binary @- && + curl -k -s -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/ | + json_pp | + grep ${HOSTNAME} | + sort | + head -n-${BACKUP_COUNT:-1} | + sed 's/.*: "\(.*\)".*/\1/' | + while read file + do + curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/${file} -X DELETE + done + data.redis: image: nanobox/redis:3.0 + cron: + - id: backup + schedule: '0 3 * * *' + command: | + curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/backup-${HOSTNAME}-$(date -u +%Y-%m-%d.%H-%M-%S).rdb --data-binary @/data/var/db/redis/dump.rdb && + curl -k -s -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/ | + json_pp | + grep ${HOSTNAME} | + sort | + head -n-${BACKUP_COUNT:-1} | + sed 's/.*: "\(.*\)".*/\1/' | + while read file + do + curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/${file} -X DELETE + done + data.storage: image: nanobox/unfs:0.9 + + cron: + - id: backup + schedule: '0 3 * * *' + command: | + tar cz -C /data/var/db/unfs/ | + curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/backup-${HOSTNAME}-$(date -u +%Y-%m-%d.%H-%M-%S).tgz --data-binary @- && + curl -k -s -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/ | + json_pp | + grep ${HOSTNAME} | + sort | + head -n-${BACKUP_COUNT:-1} | + sed 's/.*: "\(.*\)".*/\1/' | + while read file + do + curl -k -H "X-AUTH-TOKEN: ${WAREHOUSE_DATA_HOARDER_TOKEN}" https://${WAREHOUSE_DATA_HOARDER_HOST}:7410/blobs/${file} -X DELETE + done From 2d6128672fcadeb29c99551a33648b4880969d22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AE=E3=82=89?= Date: Fri, 7 Jul 2017 07:48:09 +0900 Subject: [PATCH 055/114] Togglable filter links (#4021) * Togglable filter links * Rename is_selected to selected? --- app/helpers/admin/filter_helper.rb | 12 +++++++++--- app/views/admin/accounts/index.html.haml | 24 ++++++++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 0dfa30e56df..6a57b3d6364 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -6,15 +6,21 @@ module Admin::FilterHelper FILTERS = ACCOUNT_FILTERS + REPORT_FILTERS - def filter_link_to(text, more_params) - new_url = filtered_url_for(more_params) - link_to text, new_url, class: filter_link_class(new_url) + def filter_link_to(text, link_to_params, link_class_params = link_to_params) + new_url = filtered_url_for(link_to_params) + new_class = filtered_url_for(link_class_params) + link_to text, new_url, class: filter_link_class(new_class) end def table_link_to(icon, text, path, options = {}) link_to safe_join([fa_icon(icon), text]), path, options.merge(class: 'table-action-link') end + def selected?(more_params) + new_url = filtered_url_for(more_params) + filter_link_class(new_url) == 'selected' ? true : false + end + private def filter_params(more_params) diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml index 6d2849c3211..07c8d16325c 100644 --- a/app/views/admin/accounts/index.html.haml +++ b/app/views/admin/accounts/index.html.haml @@ -6,14 +6,30 @@ %strong= t('admin.accounts.location.title') %ul %li= filter_link_to t('admin.accounts.location.all'), local: nil, remote: nil - %li= filter_link_to t('admin.accounts.location.local'), local: '1', remote: nil - %li= filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil + %li + - if selected? local: '1', remote: nil + = filter_link_to t('admin.accounts.location.local'), {local: nil, remote: nil}, {local: '1', remote: nil} + - else + = filter_link_to t('admin.accounts.location.local'), local: '1', remote: nil + %li + - if selected? remote: '1', local: nil + = filter_link_to t('admin.accounts.location.remote'), {remote: nil, local: nil}, {remote: '1', local: nil} + - else + = filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil .filter-subset %strong= t('admin.accounts.moderation.title') %ul %li= filter_link_to t('admin.accounts.moderation.all'), silenced: nil, suspended: nil - %li= filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1' - %li= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1' + %li + - if selected? silenced: '1' + = filter_link_to t('admin.accounts.moderation.silenced'), {silenced: nil}, {silenced: '1'} + - else + = filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1' + %li + - if selected? suspended: '1' + = filter_link_to t('admin.accounts.moderation.suspended'), {suspended: nil}, {suspended: '1'} + - else + = filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1' .filter-subset %strong= t('admin.accounts.order.title') %ul From 8b2cad56374b2dbb6e7a445e7917810935c45536 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 7 Jul 2017 04:02:06 +0200 Subject: [PATCH 056/114] Refactor JSON templates to be generated with ActiveModelSerializers instead of Rabl (#4090) --- Gemfile | 1 + Gemfile.lock | 9 ++ .../api/v1/accounts/credentials_controller.rb | 4 +- .../accounts/follower_accounts_controller.rb | 2 +- .../accounts/following_accounts_controller.rb | 2 +- .../v1/accounts/relationships_controller.rb | 11 +-- .../api/v1/accounts/search_controller.rb | 3 +- .../api/v1/accounts/statuses_controller.rb | 5 +- app/controllers/api/v1/accounts_controller.rb | 38 +++----- app/controllers/api/v1/apps_controller.rb | 1 + app/controllers/api/v1/blocks_controller.rb | 1 + .../api/v1/favourites_controller.rb | 5 +- .../api/v1/follow_requests_controller.rb | 1 + app/controllers/api/v1/follows_controller.rb | 2 +- .../api/v1/instances_controller.rb | 4 +- app/controllers/api/v1/media_controller.rb | 1 + app/controllers/api/v1/mutes_controller.rb | 1 + .../api/v1/notifications_controller.rb | 7 +- app/controllers/api/v1/reports_controller.rb | 3 +- app/controllers/api/v1/search_controller.rb | 3 +- .../favourited_by_accounts_controller.rb | 2 +- .../api/v1/statuses/favourites_controller.rb | 4 +- .../api/v1/statuses/mutes_controller.rb | 4 +- .../reblogged_by_accounts_controller.rb | 2 +- .../api/v1/statuses/reblogs_controller.rb | 4 +- app/controllers/api/v1/statuses_controller.rb | 16 +++- .../api/v1/timelines/home_controller.rb | 6 +- .../api/v1/timelines/public_controller.rb | 6 +- .../api/v1/timelines/tag_controller.rb | 6 +- app/lib/inline_rabl_scope.rb | 17 ---- app/lib/inline_renderer.rb | 36 +++++-- app/models/context.rb | 5 + app/models/search.rb | 5 + .../account_relationships_presenter.rb | 15 +++ .../status_relationships_presenter.rb | 19 ++++ app/serializers/rest/account_serializer.rb | 33 +++++++ .../rest/application_serializer.rb | 14 +++ app/serializers/rest/context_serializer.rb | 6 ++ app/serializers/rest/instance_serializer.rb | 30 ++++++ .../rest/media_attachment_serializer.rb | 24 +++++ .../rest/notification_serializer.rb | 12 +++ .../rest/preview_card_serializer.rb | 14 +++ .../rest/relationship_serializer.rb | 30 ++++++ app/serializers/rest/report_serializer.rb | 5 + app/serializers/rest/search_serializer.rb | 12 +++ app/serializers/rest/status_serializer.rb | 93 +++++++++++++++++++ app/services/fan_out_on_write_service.rb | 2 +- app/services/notify_service.rb | 2 +- app/views/api/v1/accounts/index.rabl | 2 - app/views/api/v1/accounts/relationship.rabl | 9 -- .../api/v1/accounts/relationships/index.rabl | 2 - app/views/api/v1/accounts/show.rabl | 12 --- app/views/api/v1/accounts/statuses/index.rabl | 2 - app/views/api/v1/apps/create.rabl | 4 - app/views/api/v1/apps/show.rabl | 3 - app/views/api/v1/blocks/index.rabl | 2 - app/views/api/v1/favourites/index.rabl | 2 - app/views/api/v1/follow_requests/index.rabl | 2 - app/views/api/v1/follows/show.rabl | 2 - app/views/api/v1/instances/show.rabl | 10 -- app/views/api/v1/media/create.rabl | 7 -- app/views/api/v1/mutes/index.rabl | 2 - app/views/api/v1/notifications/index.rabl | 2 - app/views/api/v1/notifications/show.rabl | 11 --- app/views/api/v1/reports/index.rabl | 2 - app/views/api/v1/reports/show.rabl | 2 - app/views/api/v1/search/index.rabl | 13 --- app/views/api/v1/statuses/_media.rabl | 6 -- app/views/api/v1/statuses/_mention.rabl | 4 - app/views/api/v1/statuses/_show.rabl | 29 ------ app/views/api/v1/statuses/_tags.rabl | 2 - app/views/api/v1/statuses/accounts.rabl | 2 - app/views/api/v1/statuses/card.rabl | 7 -- app/views/api/v1/statuses/context.rabl | 9 -- app/views/api/v1/statuses/index.rabl | 2 - app/views/api/v1/statuses/show.rabl | 15 --- app/views/api/v1/timelines/show.rabl | 2 - app/views/home/initial_state.json.rabl | 4 +- app/workers/push_update_worker.rb | 2 +- spec/lib/inline_rabl_scope_spec.rb | 23 ----- 80 files changed, 425 insertions(+), 301 deletions(-) delete mode 100644 app/lib/inline_rabl_scope.rb create mode 100644 app/models/context.rb create mode 100644 app/models/search.rb create mode 100644 app/presenters/account_relationships_presenter.rb create mode 100644 app/presenters/status_relationships_presenter.rb create mode 100644 app/serializers/rest/account_serializer.rb create mode 100644 app/serializers/rest/application_serializer.rb create mode 100644 app/serializers/rest/context_serializer.rb create mode 100644 app/serializers/rest/instance_serializer.rb create mode 100644 app/serializers/rest/media_attachment_serializer.rb create mode 100644 app/serializers/rest/notification_serializer.rb create mode 100644 app/serializers/rest/preview_card_serializer.rb create mode 100644 app/serializers/rest/relationship_serializer.rb create mode 100644 app/serializers/rest/report_serializer.rb create mode 100644 app/serializers/rest/search_serializer.rb create mode 100644 app/serializers/rest/status_serializer.rb delete mode 100644 app/views/api/v1/accounts/index.rabl delete mode 100644 app/views/api/v1/accounts/relationship.rabl delete mode 100644 app/views/api/v1/accounts/relationships/index.rabl delete mode 100644 app/views/api/v1/accounts/show.rabl delete mode 100644 app/views/api/v1/accounts/statuses/index.rabl delete mode 100644 app/views/api/v1/apps/create.rabl delete mode 100644 app/views/api/v1/apps/show.rabl delete mode 100644 app/views/api/v1/blocks/index.rabl delete mode 100644 app/views/api/v1/favourites/index.rabl delete mode 100644 app/views/api/v1/follow_requests/index.rabl delete mode 100644 app/views/api/v1/follows/show.rabl delete mode 100644 app/views/api/v1/instances/show.rabl delete mode 100644 app/views/api/v1/media/create.rabl delete mode 100644 app/views/api/v1/mutes/index.rabl delete mode 100644 app/views/api/v1/notifications/index.rabl delete mode 100644 app/views/api/v1/notifications/show.rabl delete mode 100644 app/views/api/v1/reports/index.rabl delete mode 100644 app/views/api/v1/reports/show.rabl delete mode 100644 app/views/api/v1/search/index.rabl delete mode 100644 app/views/api/v1/statuses/_media.rabl delete mode 100644 app/views/api/v1/statuses/_mention.rabl delete mode 100644 app/views/api/v1/statuses/_show.rabl delete mode 100644 app/views/api/v1/statuses/_tags.rabl delete mode 100644 app/views/api/v1/statuses/accounts.rabl delete mode 100644 app/views/api/v1/statuses/card.rabl delete mode 100644 app/views/api/v1/statuses/context.rabl delete mode 100644 app/views/api/v1/statuses/index.rabl delete mode 100644 app/views/api/v1/statuses/show.rabl delete mode 100644 app/views/api/v1/timelines/show.rabl delete mode 100644 spec/lib/inline_rabl_scope_spec.rb diff --git a/Gemfile b/Gemfile index 6ee884a17a4..95c74eef900 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,7 @@ gem 'aws-sdk', '~> 2.9' gem 'paperclip', '~> 5.1' gem 'paperclip-av-transcoder', '~> 0.6' +gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.5' gem 'bootsnap' gem 'browser' diff --git a/Gemfile.lock b/Gemfile.lock index f0156529ca5..71f83f73667 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -24,6 +24,11 @@ GEM erubi (~> 1.4) rails-dom-testing (~> 2.0) 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) activejob (5.1.2) activesupport (= 5.1.2) @@ -101,6 +106,8 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + case_transform (0.2) + activesupport chunky_png (1.3.8) cld3 (3.1.3) ffi (>= 1.1.0, < 1.10.0) @@ -200,6 +207,7 @@ GEM terminal-table (>= 1.5.1) jmespath (1.3.1) json (2.1.0) + jsonapi-renderer (0.1.2) kaminari (1.0.1) activesupport (>= 4.1.0) kaminari-actionview (= 1.0.1) @@ -476,6 +484,7 @@ PLATFORMS ruby DEPENDENCIES + active_model_serializers (~> 0.10) active_record_query_trace (~> 1.5) addressable (~> 2.5) annotate (~> 2.7) diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 1cf52ff10c5..8ee9a2416e9 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -6,13 +6,13 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController def show @account = current_account - render 'api/v1/accounts/show' + render json: @account, serializer: REST::AccountSerializer end def update current_account.update!(account_params) @account = current_account - render 'api/v1/accounts/show' + render json: @account, serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/accounts/follower_accounts_controller.rb b/app/controllers/api/v1/accounts/follower_accounts_controller.rb index 81aae56d3f6..80b0bef4072 100644 --- a/app/controllers/api/v1/accounts/follower_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/follower_accounts_controller.rb @@ -9,7 +9,7 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController def index @accounts = load_accounts - render 'api/v1/accounts/index' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/accounts/following_accounts_controller.rb b/app/controllers/api/v1/accounts/following_accounts_controller.rb index 63c6d54b29c..55cffdf37c1 100644 --- a/app/controllers/api/v1/accounts/following_accounts_controller.rb +++ b/app/controllers/api/v1/accounts/following_accounts_controller.rb @@ -9,7 +9,7 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController def index @accounts = load_accounts - render 'api/v1/accounts/index' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index cb923ab9177..a88cf2021ac 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -8,16 +8,15 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController def index @accounts = Account.where(id: account_ids).select('id') - @following = Account.following_map(account_ids, current_user.account_id) - @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) + render json: @accounts, each_serializer: REST::RelationshipSerializer, relationships: relationships end private + def relationships + AccountRelationshipsPresenter.new(@accounts, current_user.account_id) + end + def account_ids @_account_ids ||= Array(params[:id]).map(&:to_i) end diff --git a/app/controllers/api/v1/accounts/search_controller.rb b/app/controllers/api/v1/accounts/search_controller.rb index c4a8f97f248..2a5cac547bb 100644 --- a/app/controllers/api/v1/accounts/search_controller.rb +++ b/app/controllers/api/v1/accounts/search_controller.rb @@ -8,8 +8,7 @@ class Api::V1::Accounts::SearchController < Api::BaseController def show @accounts = account_search - - render 'api/v1/accounts/index' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/accounts/statuses_controller.rb b/app/controllers/api/v1/accounts/statuses_controller.rb index 504ed8c07d3..d9ae5c0896b 100644 --- a/app/controllers/api/v1/accounts/statuses_controller.rb +++ b/app/controllers/api/v1/accounts/statuses_controller.rb @@ -9,6 +9,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController def index @statuses = load_statuses + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private @@ -18,9 +19,7 @@ class Api::V1::Accounts::StatusesController < Api::BaseController end def load_statuses - cached_account_statuses.tap do |statuses| - set_maps(statuses) - end + cached_account_statuses end def cached_account_statuses diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 8fc0dd36f59..f621aa245df 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -8,49 +8,38 @@ class Api::V1::AccountsController < Api::BaseController respond_to :json - def show; end + def show + render json: @account, serializer: REST::AccountSerializer + end def follow FollowService.new.call(current_user.account, @account.acct) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def block BlockService.new.call(current_user.account, @account) - - @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 + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def mute MuteService.new.call(current_user.account, @account) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def unfollow UnfollowService.new.call(current_user.account, @account) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def unblock UnblockService.new.call(current_user.account, @account) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end def unmute UnmuteService.new.call(current_user.account, @account) - set_relationship - render :relationship + render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end private @@ -59,12 +48,7 @@ class Api::V1::AccountsController < Api::BaseController @account = Account.find(params[:id]) end - def set_relationship - @following = Account.following_map([@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) + def relationships + AccountRelationshipsPresenter.new([@account.id], current_user.account_id) end end diff --git a/app/controllers/api/v1/apps_controller.rb b/app/controllers/api/v1/apps_controller.rb index 98e90894896..44a27b20a22 100644 --- a/app/controllers/api/v1/apps_controller.rb +++ b/app/controllers/api/v1/apps_controller.rb @@ -5,6 +5,7 @@ class Api::V1::AppsController < Api::BaseController def create @app = Doorkeeper::Application.create!(application_options) + render json: @app, serializer: REST::ApplicationSerializer end private diff --git a/app/controllers/api/v1/blocks_controller.rb b/app/controllers/api/v1/blocks_controller.rb index 1702953cf73..a412e434145 100644 --- a/app/controllers/api/v1/blocks_controller.rb +++ b/app/controllers/api/v1/blocks_controller.rb @@ -9,6 +9,7 @@ class Api::V1::BlocksController < Api::BaseController def index @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/favourites_controller.rb b/app/controllers/api/v1/favourites_controller.rb index fe0819a3f59..92c0a62a980 100644 --- a/app/controllers/api/v1/favourites_controller.rb +++ b/app/controllers/api/v1/favourites_controller.rb @@ -9,14 +9,13 @@ class Api::V1::FavouritesController < Api::BaseController def index @statuses = load_statuses + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private def load_statuses - cached_favourites.tap do |statuses| - set_maps(statuses) - end + cached_favourites end def cached_favourites diff --git a/app/controllers/api/v1/follow_requests_controller.rb b/app/controllers/api/v1/follow_requests_controller.rb index eed22ef4fba..b9f50d78438 100644 --- a/app/controllers/api/v1/follow_requests_controller.rb +++ b/app/controllers/api/v1/follow_requests_controller.rb @@ -7,6 +7,7 @@ class Api::V1::FollowRequestsController < Api::BaseController def index @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer end def authorize diff --git a/app/controllers/api/v1/follows_controller.rb b/app/controllers/api/v1/follows_controller.rb index bcdb4e177a7..e01ae5c01cc 100644 --- a/app/controllers/api/v1/follows_controller.rb +++ b/app/controllers/api/v1/follows_controller.rb @@ -10,7 +10,7 @@ class Api::V1::FollowsController < Api::BaseController raise ActiveRecord::RecordNotFound if follow_params[:uri].blank? @account = FollowService.new.call(current_user.account, target_uri).try(:target_account) - render :show + render json: @account, serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index ce2181879be..1c6971c1828 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -3,5 +3,7 @@ class Api::V1::InstancesController < Api::BaseController respond_to :json - def show; end + def show + render json: {}, serializer: REST::InstanceSerializer + end end diff --git a/app/controllers/api/v1/media_controller.rb b/app/controllers/api/v1/media_controller.rb index 25a3313195c..8a1992fca41 100644 --- a/app/controllers/api/v1/media_controller.rb +++ b/app/controllers/api/v1/media_controller.rb @@ -11,6 +11,7 @@ class Api::V1::MediaController < Api::BaseController def create @media = current_account.media_attachments.create!(file: media_params[:file]) + render json: @media, serializer: REST::MediaAttachmentSerializer rescue Paperclip::Errors::NotIdentifiedByImageMagickError render json: file_type_error, status: 422 rescue Paperclip::Error diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index 2a353df039a..0c43cb94302 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -9,6 +9,7 @@ class Api::V1::MutesController < Api::BaseController def index @accounts = load_accounts + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index a28e99f2fb0..8910b77e931 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -11,11 +11,12 @@ class Api::V1::NotificationsController < Api::BaseController def index @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 def show @notification = current_account.notifications.find(params[:id]) + render json: @notification, serializer: REST::NotificationSerializer end def clear @@ -46,10 +47,6 @@ class Api::V1::NotificationsController < Api::BaseController current_account.notifications.browserable(exclude_types) end - def set_maps_for_notification_target_statuses - set_maps target_statuses_from_notifications - end - def target_statuses_from_notifications @notifications.reject { |notification| notification.target_status.nil? }.map(&:target_status) end diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb index 8e7070d0779..9592cd4bdc5 100644 --- a/app/controllers/api/v1/reports_controller.rb +++ b/app/controllers/api/v1/reports_controller.rb @@ -9,6 +9,7 @@ class Api::V1::ReportsController < Api::BaseController def index @reports = current_account.reports + render json: @reports, each_serializer: REST::ReportSerializer end 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 } - render :show + render json: @report, serializer: REST::ReportSerializer end private diff --git a/app/controllers/api/v1/search_controller.rb b/app/controllers/api/v1/search_controller.rb index 8b832148c30..1353682eaf6 100644 --- a/app/controllers/api/v1/search_controller.rb +++ b/app/controllers/api/v1/search_controller.rb @@ -6,7 +6,8 @@ class Api::V1::SearchController < Api::BaseController respond_to :json def index - @search = OpenStruct.new(search_results) + @search = Search.new(search_results) + render json: @search, serializer: REST::SearchSerializer end private diff --git a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb index e5818493952..f95cf9457ff 100644 --- a/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/favourited_by_accounts_controller.rb @@ -11,7 +11,7 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController def index @accounts = load_accounts - render 'api/v1/statuses/accounts' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/statuses/favourites_controller.rb b/app/controllers/api/v1/statuses/favourites_controller.rb index b6fb13cc073..4c4b0c1604a 100644 --- a/app/controllers/api/v1/statuses/favourites_controller.rb +++ b/app/controllers/api/v1/statuses/favourites_controller.rb @@ -10,7 +10,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController def create @status = favourited_status - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end def destroy @@ -19,7 +19,7 @@ class Api::V1::Statuses::FavouritesController < Api::BaseController UnfavouriteWorker.perform_async(current_user.account_id, @status.id) - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end private diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb index eab88f2efdb..a4bf0acdd26 100644 --- a/app/controllers/api/v1/statuses/mutes_controller.rb +++ b/app/controllers/api/v1/statuses/mutes_controller.rb @@ -14,14 +14,14 @@ class Api::V1::Statuses::MutesController < Api::BaseController current_account.mute_conversation!(@conversation) @mutes_map = { @conversation.id => true } - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end def destroy current_account.unmute_conversation!(@conversation) @mutes_map = { @conversation.id => false } - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end private diff --git a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb index 43593d3c5a7..175217e6eb1 100644 --- a/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb +++ b/app/controllers/api/v1/statuses/reblogged_by_accounts_controller.rb @@ -11,7 +11,7 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController def index @accounts = load_accounts - render 'api/v1/statuses/accounts' + render json: @accounts, each_serializer: REST::AccountSerializer end private diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb index ee9c5b3a624..f7f4b5a5c6d 100644 --- a/app/controllers/api/v1/statuses/reblogs_controller.rb +++ b/app/controllers/api/v1/statuses/reblogs_controller.rb @@ -10,7 +10,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController def create @status = ReblogService.new.call(current_user.account, status_for_reblog) - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end def destroy @@ -20,7 +20,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController authorize status_for_destroy, :unreblog? RemovalWorker.perform_async(status_for_destroy.id) - render 'api/v1/statuses/show' + render json: @status, serializer: REST::StatusSerializer end private diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 9aa1cbc4d17..9c7124d0f0c 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -13,6 +13,7 @@ class Api::V1::StatusesController < Api::BaseController def show cached = Rails.cache.read(@status.cache_key) @status = cached unless cached.nil? + render json: @status, serializer: REST::StatusSerializer end def context @@ -21,15 +22,20 @@ class Api::V1::StatusesController < Api::BaseController loaded_ancestors = cache_collection(ancestors_results, Status) loaded_descendants = cache_collection(descendants_results, Status) - @context = OpenStruct.new(ancestors: loaded_ancestors, descendants: loaded_descendants) - statuses = [@status] + @context[:ancestors] + @context[:descendants] + @context = Context.new(ancestors: loaded_ancestors, descendants: loaded_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 def card @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 def create @@ -43,7 +49,7 @@ class Api::V1::StatusesController < Api::BaseController application: doorkeeper_token.application, idempotency: request.headers['Idempotency-Key']) - render :show + render json: @status, serializer: REST::StatusSerializer end def destroy diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb index 511d2f65da2..3dd27710cb6 100644 --- a/app/controllers/api/v1/timelines/home_controller.rb +++ b/app/controllers/api/v1/timelines/home_controller.rb @@ -9,15 +9,13 @@ class Api::V1::Timelines::HomeController < Api::BaseController def show @statuses = load_statuses - render 'api/v1/timelines/show' + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private def load_statuses - cached_home_statuses.tap do |statuses| - set_maps(statuses) - end + cached_home_statuses end def cached_home_statuses diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index 305451cc7b7..49887778e09 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -7,15 +7,13 @@ class Api::V1::Timelines::PublicController < Api::BaseController def show @statuses = load_statuses - render 'api/v1/timelines/show' + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private def load_statuses - cached_public_statuses.tap do |statuses| - set_maps(statuses) - end + cached_public_statuses end def cached_public_statuses diff --git a/app/controllers/api/v1/timelines/tag_controller.rb b/app/controllers/api/v1/timelines/tag_controller.rb index 50afca7c723..08db04a39d2 100644 --- a/app/controllers/api/v1/timelines/tag_controller.rb +++ b/app/controllers/api/v1/timelines/tag_controller.rb @@ -8,7 +8,7 @@ class Api::V1::Timelines::TagController < Api::BaseController def show @statuses = load_statuses - render 'api/v1/timelines/show' + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) end private @@ -18,9 +18,7 @@ class Api::V1::Timelines::TagController < Api::BaseController end def load_statuses - cached_tagged_statuses.tap do |statuses| - set_maps(statuses) - end + cached_tagged_statuses end def cached_tagged_statuses diff --git a/app/lib/inline_rabl_scope.rb b/app/lib/inline_rabl_scope.rb deleted file mode 100644 index 26adcb03afd..00000000000 --- a/app/lib/inline_rabl_scope.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -class InlineRablScope - include RoutingHelper - - def initialize(account) - @account = account - end - - def current_user - @account.try(:user) - end - - def current_account - @account - end -end diff --git a/app/lib/inline_renderer.rb b/app/lib/inline_renderer.rb index 8e04ad1d551..7cd9758ece5 100644 --- a/app/lib/inline_renderer.rb +++ b/app/lib/inline_renderer.rb @@ -1,13 +1,33 @@ # frozen_string_literal: true class InlineRenderer - def self.render(status, current_account, template) - Rabl::Renderer.new( - template, - status, - view_path: 'app/views', - format: :json, - scope: InlineRablScope.new(current_account) - ).render + def initialize(object, current_account, template) + @object = object + @current_account = current_account + @template = template + end + + def render + case @template + when :status + serializer = REST::StatusSerializer + when :notification + serializer = REST::NotificationSerializer + else + return + end + + serializable_resource = ActiveModelSerializers::SerializableResource.new(@object, serializer: serializer, scope: current_user, scope_name: :current_user) + serializable_resource.as_json + end + + def self.render(object, current_account, template) + new(object, current_account, template).render + end + + private + + def current_user + @current_account&.user end end diff --git a/app/models/context.rb b/app/models/context.rb new file mode 100644 index 00000000000..cc667999ed9 --- /dev/null +++ b/app/models/context.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Context < ActiveModelSerializers::Model + attributes :ancestors, :descendants +end diff --git a/app/models/search.rb b/app/models/search.rb new file mode 100644 index 00000000000..676c2a7f8e8 --- /dev/null +++ b/app/models/search.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Search < ActiveModelSerializers::Model + attributes :accounts, :statuses, :hashtags +end diff --git a/app/presenters/account_relationships_presenter.rb b/app/presenters/account_relationships_presenter.rb new file mode 100644 index 00000000000..65780786397 --- /dev/null +++ b/app/presenters/account_relationships_presenter.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AccountRelationshipsPresenter + attr_reader :following, :followed_by, :blocking, + :muting, :requested, :domain_blocking + + def initialize(account_ids, current_account_id) + @following = Account.following_map(account_ids, current_account_id) + @followed_by = Account.followed_by_map(account_ids, current_account_id) + @blocking = Account.blocking_map(account_ids, current_account_id) + @muting = Account.muting_map(account_ids, current_account_id) + @requested = Account.requested_map(account_ids, current_account_id) + @domain_blocking = Account.domain_blocking_map(account_ids, current_account_id) + end +end diff --git a/app/presenters/status_relationships_presenter.rb b/app/presenters/status_relationships_presenter.rb new file mode 100644 index 00000000000..caf00791a3f --- /dev/null +++ b/app/presenters/status_relationships_presenter.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class StatusRelationshipsPresenter + attr_reader :reblogs_map, :favourites_map, :mutes_map + + def initialize(statuses, current_account_id = nil) + if current_account_id.nil? + @reblogs_map = {} + @favourites_map = {} + @mutes_map = {} + else + status_ids = statuses.compact.flat_map { |s| [s.id, s.reblog_of_id] }.uniq + conversation_ids = statuses.compact.map(&:conversation_id).compact.uniq + @reblogs_map = Status.reblogs_map(status_ids, current_account_id) + @favourites_map = Status.favourites_map(status_ids, current_account_id) + @mutes_map = Status.mutes_map(conversation_ids, current_account_id) + end + end +end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb new file mode 100644 index 00000000000..012a4fd18aa --- /dev/null +++ b/app/serializers/rest/account_serializer.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class REST::AccountSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :id, :username, :acct, :display_name, :locked, :created_at, + :note, :url, :avatar, :avatar_static, :header, :header_static, + :followers_count, :following_count, :statuses_count + + def note + Formatter.instance.simplified_format(object) + end + + def url + TagManager.instance.url_for(object) + end + + def avatar + full_asset_url(object.avatar_original_url) + end + + def avatar_static + full_asset_url(object.avatar_static_url) + end + + def header + full_asset_url(object.header_original_url) + end + + def header_static + full_asset_url(object.header_static_url) + end +end diff --git a/app/serializers/rest/application_serializer.rb b/app/serializers/rest/application_serializer.rb new file mode 100644 index 00000000000..868a62f1e73 --- /dev/null +++ b/app/serializers/rest/application_serializer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class REST::ApplicationSerializer < ActiveModel::Serializer + attributes :id, :name, :website, :redirect_uri, + :client_id, :client_secret + + def client_id + object.uid + end + + def client_secret + object.secret + end +end diff --git a/app/serializers/rest/context_serializer.rb b/app/serializers/rest/context_serializer.rb new file mode 100644 index 00000000000..44515c85d7d --- /dev/null +++ b/app/serializers/rest/context_serializer.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class REST::ContextSerializer < ActiveModel::Serializer + has_many :ancestors, serializer: REST::StatusSerializer + has_many :descendants, serializer: REST::StatusSerializer +end diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb new file mode 100644 index 00000000000..8e32f9cb35e --- /dev/null +++ b/app/serializers/rest/instance_serializer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class REST::InstanceSerializer < ActiveModel::Serializer + attributes :uri, :title, :description, :email, + :version, :urls + + def uri + Rails.configuration.x.local_domain + end + + def title + Setting.site_title + end + + def description + Setting.site_description + end + + def email + Setting.site_contact_email + end + + def version + Mastodon::Version.to_s + end + + def urls + { streaming_api: Rails.configuration.x.streaming_api_base_url } + end +end diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb new file mode 100644 index 00000000000..9b07a686ef3 --- /dev/null +++ b/app/serializers/rest/media_attachment_serializer.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class REST::MediaAttachmentSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :id, :type, :url, :preview_url, + :remote_url, :text_url, :meta + + def url + full_asset_url(object.file.url(:original)) + end + + def preview_url + full_asset_url(object.file.url(:small)) + end + + def text_url + medium_url(object.id) + end + + def meta + object.file.meta + end +end diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb new file mode 100644 index 00000000000..97fadf32edf --- /dev/null +++ b/app/serializers/rest/notification_serializer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class REST::NotificationSerializer < ActiveModel::Serializer + attributes :id, :type, :created_at + + belongs_to :from_account, key: :account, serializer: REST::AccountSerializer + belongs_to :status, if: :status_type?, serializer: REST::StatusSerializer + + def status_type? + [:favourite, :reblog, :mention].include?(object.type) + end +end diff --git a/app/serializers/rest/preview_card_serializer.rb b/app/serializers/rest/preview_card_serializer.rb new file mode 100644 index 00000000000..9c460332c25 --- /dev/null +++ b/app/serializers/rest/preview_card_serializer.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class REST::PreviewCardSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :url, :title, :description, :type, + :author_name, :author_url, :provider_name, + :provider_url, :html, :width, :height, + :image + + def image + object.image? ? full_asset_url(object.image.url(:original)) : nil + end +end diff --git a/app/serializers/rest/relationship_serializer.rb b/app/serializers/rest/relationship_serializer.rb new file mode 100644 index 00000000000..1d431aa1b66 --- /dev/null +++ b/app/serializers/rest/relationship_serializer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class REST::RelationshipSerializer < ActiveModel::Serializer + attributes :id, :following, :followed_by, :blocking, + :muting, :requested, :domain_blocking + + def following + instance_options[:relationships].following[object.id] || false + end + + def followed_by + instance_options[:relationships].followed_by[object.id] || false + end + + def blocking + instance_options[:relationships].blocking[object.id] || false + end + + def muting + instance_options[:relationships].muting[object.id] || false + end + + def requested + instance_options[:relationships].requested[object.id] || false + end + + def domain_blocking + instance_options[:relationships].domain_blocking[object.id] || false + end +end diff --git a/app/serializers/rest/report_serializer.rb b/app/serializers/rest/report_serializer.rb new file mode 100644 index 00000000000..0c6bd65567d --- /dev/null +++ b/app/serializers/rest/report_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class REST::ReportSerializer < ActiveModel::Serializer + attributes :id, :action_taken +end diff --git a/app/serializers/rest/search_serializer.rb b/app/serializers/rest/search_serializer.rb new file mode 100644 index 00000000000..157f543aef7 --- /dev/null +++ b/app/serializers/rest/search_serializer.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class REST::SearchSerializer < ActiveModel::Serializer + attributes :hashtags + + has_many :accounts, serializer: REST::AccountSerializer + has_many :statuses, serializer: REST::StatusSerializer + + def hashtags + object.hashtags.map(&:name) + end +end diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb new file mode 100644 index 00000000000..246b12a909c --- /dev/null +++ b/app/serializers/rest/status_serializer.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +class REST::StatusSerializer < ActiveModel::Serializer + attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, + :sensitive, :spoiler_text, :visibility, :language, + :uri, :content, :url, :reblogs_count, :favourites_count + + attribute :favourited, if: :current_user? + attribute :reblogged, if: :current_user? + attribute :muted, if: :current_user? + + belongs_to :reblog, serializer: REST::StatusSerializer + belongs_to :application + belongs_to :account, serializer: REST::AccountSerializer + + has_many :media_attachments, serializer: REST::MediaAttachmentSerializer + has_many :mentions + has_many :tags + + def current_user? + !current_user.nil? + end + + def uri + TagManager.instance.uri_for(object) + end + + def content + Formatter.instance.format(object) + end + + def url + TagManager.instance.url_for(object) + end + + def favourited + if instance_options && instance_options[:relationships] + instance_options[:relationships].favourites_map[object.id] || false + else + current_user.account.favourited?(object) + end + end + + def reblogged + if instance_options && instance_options[:relationships] + instance_options[:relationships].reblogs_map[object.id] || false + else + current_user.account.reblogged?(object) + end + end + + def muted + if instance_options && instance_options[:relationships] + instance_options[:relationships].mutes_map[object.conversation_id] || false + else + current_user.account.muting_conversation?(object.conversation) + end + end + + class ApplicationSerializer < ActiveModel::Serializer + attributes :name, :website + end + + class MentionSerializer < ActiveModel::Serializer + attributes :id, :username, :url, :acct + + def id + object.account_id + end + + def username + object.account_username + end + + def url + TagManager.instance.url_for(object.account) + end + + def acct + object.account_acct + end + end + + class TagSerializer < ActiveModel::Serializer + include RoutingHelper + + attributes :name, :url + + def url + tag_url(object) + end + end +end diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 3b74696d5b0..47a47a73546 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -54,7 +54,7 @@ class FanOutOnWriteService < BaseService end def render_anonymous_payload(status) - @payload = InlineRenderer.render(status, nil, 'api/v1/statuses/show') + @payload = InlineRenderer.render(status, nil, :status) @payload = Oj.dump(event: :update, payload: @payload) end diff --git a/app/services/notify_service.rb b/app/services/notify_service.rb index 422d5f97e54..407d385ea24 100644 --- a/app/services/notify_service.rb +++ b/app/services/notify_service.rb @@ -60,7 +60,7 @@ class NotifyService < BaseService def create_notification @notification.save! return unless @notification.browserable? - Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, 'api/v1/notifications/show'))) + Redis.current.publish("timeline:#{@recipient.id}", Oj.dump(event: :notification, payload: InlineRenderer.render(@notification, @recipient, :notification))) end def send_email diff --git a/app/views/api/v1/accounts/index.rabl b/app/views/api/v1/accounts/index.rabl deleted file mode 100644 index 9f3b13a53d9..00000000000 --- a/app/views/api/v1/accounts/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/accounts/relationship.rabl b/app/views/api/v1/accounts/relationship.rabl deleted file mode 100644 index 4f7763d9d0e..00000000000 --- a/app/views/api/v1/accounts/relationship.rabl +++ /dev/null @@ -1,9 +0,0 @@ -object @account - -attribute :id -node(:following) { |account| @following[account.id] || false } -node(:followed_by) { |account| @followed_by[account.id] || false } -node(:blocking) { |account| @blocking[account.id] || false } -node(:muting) { |account| @muting[account.id] || false } -node(:requested) { |account| @requested[account.id] || false } -node(:domain_blocking) { |account| @domain_blocking[account.id] || false } diff --git a/app/views/api/v1/accounts/relationships/index.rabl b/app/views/api/v1/accounts/relationships/index.rabl deleted file mode 100644 index 022ea2ac499..00000000000 --- a/app/views/api/v1/accounts/relationships/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/relationship' diff --git a/app/views/api/v1/accounts/show.rabl b/app/views/api/v1/accounts/show.rabl deleted file mode 100644 index 8826aa22da4..00000000000 --- a/app/views/api/v1/accounts/show.rabl +++ /dev/null @@ -1,12 +0,0 @@ -object @account - -attributes :id, :username, :acct, :display_name, :locked, :created_at - -node(:note) { |account| Formatter.instance.simplified_format(account) } -node(:url) { |account| TagManager.instance.url_for(account) } -node(:avatar) { |account| full_asset_url(account.avatar_original_url) } -node(:avatar_static) { |account| full_asset_url(account.avatar_static_url) } -node(:header) { |account| full_asset_url(account.header_original_url) } -node(:header_static) { |account| full_asset_url(account.header_static_url) } - -attributes :followers_count, :following_count, :statuses_count diff --git a/app/views/api/v1/accounts/statuses/index.rabl b/app/views/api/v1/accounts/statuses/index.rabl deleted file mode 100644 index 44d29d91ba2..00000000000 --- a/app/views/api/v1/accounts/statuses/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @statuses -extends 'api/v1/statuses/show' diff --git a/app/views/api/v1/apps/create.rabl b/app/views/api/v1/apps/create.rabl deleted file mode 100644 index 1ff6469a46d..00000000000 --- a/app/views/api/v1/apps/create.rabl +++ /dev/null @@ -1,4 +0,0 @@ -object @app -attributes :id, :redirect_uri -node(:client_id) { |app| app.uid } -node(:client_secret) { |app| app.secret } diff --git a/app/views/api/v1/apps/show.rabl b/app/views/api/v1/apps/show.rabl deleted file mode 100644 index 6d9e607db99..00000000000 --- a/app/views/api/v1/apps/show.rabl +++ /dev/null @@ -1,3 +0,0 @@ -object @application - -attributes :name, :website diff --git a/app/views/api/v1/blocks/index.rabl b/app/views/api/v1/blocks/index.rabl deleted file mode 100644 index 9f3b13a53d9..00000000000 --- a/app/views/api/v1/blocks/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/favourites/index.rabl b/app/views/api/v1/favourites/index.rabl deleted file mode 100644 index 44d29d91ba2..00000000000 --- a/app/views/api/v1/favourites/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @statuses -extends 'api/v1/statuses/show' diff --git a/app/views/api/v1/follow_requests/index.rabl b/app/views/api/v1/follow_requests/index.rabl deleted file mode 100644 index 9f3b13a53d9..00000000000 --- a/app/views/api/v1/follow_requests/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/follows/show.rabl b/app/views/api/v1/follows/show.rabl deleted file mode 100644 index e0710616452..00000000000 --- a/app/views/api/v1/follows/show.rabl +++ /dev/null @@ -1,2 +0,0 @@ -object @account -extends('api/v1/accounts/show') diff --git a/app/views/api/v1/instances/show.rabl b/app/views/api/v1/instances/show.rabl deleted file mode 100644 index 05fb650315a..00000000000 --- a/app/views/api/v1/instances/show.rabl +++ /dev/null @@ -1,10 +0,0 @@ -object false - -node(:uri) { site_hostname } -node(:title) { Setting.site_title } -node(:description) { Setting.site_description } -node(:email) { Setting.site_contact_email } -node(:version) { Mastodon::Version.to_s } -node :urls do - { :streaming_api => Rails.configuration.x.streaming_api_base_url } -end diff --git a/app/views/api/v1/media/create.rabl b/app/views/api/v1/media/create.rabl deleted file mode 100644 index 53c13bbda1d..00000000000 --- a/app/views/api/v1/media/create.rabl +++ /dev/null @@ -1,7 +0,0 @@ -object @media -attribute :id, :type - -node(:url) { |media| full_asset_url(media.file.url(:original)) } -node(:preview_url) { |media| full_asset_url(media.file.url(:small)) } -node(:text_url) { |media| medium_url(media) } -node(:meta) { |media| media.file.meta } diff --git a/app/views/api/v1/mutes/index.rabl b/app/views/api/v1/mutes/index.rabl deleted file mode 100644 index 9f3b13a53d9..00000000000 --- a/app/views/api/v1/mutes/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/notifications/index.rabl b/app/views/api/v1/notifications/index.rabl deleted file mode 100644 index 6abc3da365e..00000000000 --- a/app/views/api/v1/notifications/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @notifications -extends 'api/v1/notifications/show' diff --git a/app/views/api/v1/notifications/show.rabl b/app/views/api/v1/notifications/show.rabl deleted file mode 100644 index ca34f2d5d6c..00000000000 --- a/app/views/api/v1/notifications/show.rabl +++ /dev/null @@ -1,11 +0,0 @@ -object @notification - -attributes :id, :type, :created_at - -child from_account: :account do - extends 'api/v1/accounts/show' -end - -node(:status, if: lambda { |n| [:favourite, :reblog, :mention].include?(n.type) }) do |n| - partial 'api/v1/statuses/show', object: n.target_status -end diff --git a/app/views/api/v1/reports/index.rabl b/app/views/api/v1/reports/index.rabl deleted file mode 100644 index 4f07940270d..00000000000 --- a/app/views/api/v1/reports/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @reports -extends 'api/v1/reports/show' diff --git a/app/views/api/v1/reports/show.rabl b/app/views/api/v1/reports/show.rabl deleted file mode 100644 index 006db51e317..00000000000 --- a/app/views/api/v1/reports/show.rabl +++ /dev/null @@ -1,2 +0,0 @@ -object @report -attributes :id, :action_taken diff --git a/app/views/api/v1/search/index.rabl b/app/views/api/v1/search/index.rabl deleted file mode 100644 index 8d1640f2d33..00000000000 --- a/app/views/api/v1/search/index.rabl +++ /dev/null @@ -1,13 +0,0 @@ -object @search - -child :accounts, object_root: false do - extends 'api/v1/accounts/show' -end - -node(:hashtags) do |search| - search.hashtags.map(&:name) -end - -child :statuses, object_root: false do - extends 'api/v1/statuses/show' -end diff --git a/app/views/api/v1/statuses/_media.rabl b/app/views/api/v1/statuses/_media.rabl deleted file mode 100644 index 07ac31888db..00000000000 --- a/app/views/api/v1/statuses/_media.rabl +++ /dev/null @@ -1,6 +0,0 @@ -attributes :id, :remote_url, :type - -node(:url) { |media| full_asset_url(media.file.url(:original)) } -node(:preview_url) { |media| full_asset_url(media.file.url(:small)) } -node(:text_url) { |media| media.local? ? medium_url(media) : nil } -node(:meta) { |media| media.file.meta } diff --git a/app/views/api/v1/statuses/_mention.rabl b/app/views/api/v1/statuses/_mention.rabl deleted file mode 100644 index 8c95fc9bde7..00000000000 --- a/app/views/api/v1/statuses/_mention.rabl +++ /dev/null @@ -1,4 +0,0 @@ -node(:url) { |mention| TagManager.instance.url_for(mention.account) } -node(:acct) { |mention| mention.account_acct } -node(:id) { |mention| mention.account_id } -node(:username) { |mention| mention.account_username } diff --git a/app/views/api/v1/statuses/_show.rabl b/app/views/api/v1/statuses/_show.rabl deleted file mode 100644 index fe3ec89ab2d..00000000000 --- a/app/views/api/v1/statuses/_show.rabl +++ /dev/null @@ -1,29 +0,0 @@ -attributes :id, :created_at, :in_reply_to_id, - :in_reply_to_account_id, :sensitive, - :spoiler_text, :visibility, :language - -node(:uri) { |status| TagManager.instance.uri_for(status) } -node(:content) { |status| Formatter.instance.format(status) } -node(:url) { |status| TagManager.instance.url_for(status) } -node(:reblogs_count) { |status| defined?(@reblogs_counts_map) ? (@reblogs_counts_map[status.id] || 0) : status.reblogs_count } -node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites_count } - -child :application do - extends 'api/v1/apps/show' -end - -child :account do - extends 'api/v1/accounts/show' -end - -child :media_attachments, object_root: false do - extends 'api/v1/statuses/_media' -end - -child :mentions, object_root: false do - extends 'api/v1/statuses/_mention' -end - -child :tags, object_root: false do - extends 'api/v1/statuses/_tags' -end diff --git a/app/views/api/v1/statuses/_tags.rabl b/app/views/api/v1/statuses/_tags.rabl deleted file mode 100644 index 25e7b0fac6b..00000000000 --- a/app/views/api/v1/statuses/_tags.rabl +++ /dev/null @@ -1,2 +0,0 @@ -attribute :name -node(:url) { |tag| tag_url(tag) } diff --git a/app/views/api/v1/statuses/accounts.rabl b/app/views/api/v1/statuses/accounts.rabl deleted file mode 100644 index 9f3b13a53d9..00000000000 --- a/app/views/api/v1/statuses/accounts.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @accounts -extends 'api/v1/accounts/show' diff --git a/app/views/api/v1/statuses/card.rabl b/app/views/api/v1/statuses/card.rabl deleted file mode 100644 index 5d8d7af3ba5..00000000000 --- a/app/views/api/v1/statuses/card.rabl +++ /dev/null @@ -1,7 +0,0 @@ -object @card - -attributes :url, :title, :description, :type, - :author_name, :author_url, :provider_name, - :provider_url, :html, :width, :height - -node(:image) { |card| card.image? ? full_asset_url(card.image.url(:original)) : nil } diff --git a/app/views/api/v1/statuses/context.rabl b/app/views/api/v1/statuses/context.rabl deleted file mode 100644 index 0b62f26d552..00000000000 --- a/app/views/api/v1/statuses/context.rabl +++ /dev/null @@ -1,9 +0,0 @@ -object @context - -node :ancestors do |context| - partial 'api/v1/statuses/index', object: context.ancestors -end - -node :descendants do |context| - partial 'api/v1/statuses/index', object: context.descendants -end diff --git a/app/views/api/v1/statuses/index.rabl b/app/views/api/v1/statuses/index.rabl deleted file mode 100644 index 0a0ed13c5b2..00000000000 --- a/app/views/api/v1/statuses/index.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @statuses -extends('api/v1/statuses/show') diff --git a/app/views/api/v1/statuses/show.rabl b/app/views/api/v1/statuses/show.rabl deleted file mode 100644 index 4b33fb2c316..00000000000 --- a/app/views/api/v1/statuses/show.rabl +++ /dev/null @@ -1,15 +0,0 @@ -object @status - -extends 'api/v1/statuses/_show' - -node(:favourited, if: proc { !current_account.nil? }) { |status| defined?(@favourites_map) ? @favourites_map[status.id] : current_account.favourited?(status) } -node(:reblogged, if: proc { !current_account.nil? }) { |status| defined?(@reblogs_map) ? @reblogs_map[status.id] : current_account.reblogged?(status) } -node(:muted, if: proc { !current_account.nil? }) { |status| defined?(@mutes_map) ? @mutes_map[status.conversation_id] : current_account.muting_conversation?(status.conversation) } - -child reblog: :reblog do - extends 'api/v1/statuses/_show' - - node(:favourited, if: proc { !current_account.nil? }) { |status| defined?(@favourites_map) ? @favourites_map[status.id] : current_account.favourited?(status) } - node(:reblogged, if: proc { !current_account.nil? }) { |status| defined?(@reblogs_map) ? @reblogs_map[status.id] : current_account.reblogged?(status) } - node(:muted, if: proc { !current_account.nil? }) { false } -end diff --git a/app/views/api/v1/timelines/show.rabl b/app/views/api/v1/timelines/show.rabl deleted file mode 100644 index 0a0ed13c5b2..00000000000 --- a/app/views/api/v1/timelines/show.rabl +++ /dev/null @@ -1,2 +0,0 @@ -collection @statuses -extends('api/v1/statuses/show') diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl index 291ff806ba0..c428a5a1fdd 100644 --- a/app/views/home/initial_state.json.rabl +++ b/app/views/home/initial_state.json.rabl @@ -24,8 +24,8 @@ end node(:accounts) do store = {} - store[current_account.id] = partial('api/v1/accounts/show', object: current_account) - store[@admin.id] = partial('api/v1/accounts/show', object: @admin) unless @admin.nil? + store[current_account.id] = ActiveModelSerializers::SerializableResource.new(current_account, serializer: REST::AccountSerializer) + store[@admin.id] = ActiveModelSerializers::SerializableResource.new(@admin, serializer: REST::AccountSerializer) unless @admin.nil? store end diff --git a/app/workers/push_update_worker.rb b/app/workers/push_update_worker.rb index fbcdcf63419..697cbd6a6bc 100644 --- a/app/workers/push_update_worker.rb +++ b/app/workers/push_update_worker.rb @@ -6,7 +6,7 @@ class PushUpdateWorker def perform(account_id, status_id) account = Account.find(account_id) status = Status.find(status_id) - message = InlineRenderer.render(status, account, 'api/v1/statuses/show') + message = InlineRenderer.render(status, account, :status) Redis.current.publish("timeline:#{account.id}", Oj.dump(event: :update, payload: message, queued_at: (Time.now.to_f * 1000.0).to_i)) rescue ActiveRecord::RecordNotFound diff --git a/spec/lib/inline_rabl_scope_spec.rb b/spec/lib/inline_rabl_scope_spec.rb deleted file mode 100644 index 3fff176e46c..00000000000 --- a/spec/lib/inline_rabl_scope_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe InlineRablScope do - describe '#current_account' do - it 'returns the given account' do - account = Fabricate(:account) - expect(InlineRablScope.new(account).current_account).to eq account - end - end - - describe '#current_user' do - it 'returns nil if the given account is nil' do - expect(InlineRablScope.new(nil).current_user).to eq nil - end - - it 'returns user of account if the given account is not nil' do - user = Fabricate(:user) - expect(InlineRablScope.new(user.account).current_user).to eq user - end - end -end From 1c1819a78a33cb7a90b499676c587f3c6dd7406f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 7 Jul 2017 04:31:40 +0200 Subject: [PATCH 057/114] Fix feed author not being enforced in ProcessFeedService (#4092) Ensure the only allowed author of top-level entries in feed is the person the feed belongs to (a verified user). Ensure delete events only apply if the deleted item belonged to that user. --- app/services/process_feed_service.rb | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb index fbdf92caab7..028962d5be0 100644 --- a/app/services/process_feed_service.rb +++ b/app/services/process_feed_service.rb @@ -42,7 +42,7 @@ class ProcessFeedService < BaseService private def create_status - if redis.exists("delete_upon_arrival:#{id}") + if redis.exists("delete_upon_arrival:#{@account.id}:#{id}") Rails.logger.debug "Delete for status #{id} was queued, ignoring" return end @@ -99,15 +99,13 @@ class ProcessFeedService < BaseService def delete_status Rails.logger.debug "Deleting remote status #{id}" - status = Status.find_by(uri: id) + status = Status.find_by(uri: id, account: @account) if status.nil? - redis.setex("delete_upon_arrival:#{id}", 6 * 3_600, id) + redis.setex("delete_upon_arrival:#{@account.id}:#{id}", 6 * 3_600, id) else RemoveStatusService.new.call(status) end - - nil end def skip_unsupported_type? @@ -128,18 +126,7 @@ class ProcessFeedService < BaseService return [status, false] unless status.nil? - # If status embeds an author, find that author - # If that author cannot be found, don't record the status (do not misattribute) - if account?(entry) - begin - account = author_from_xml(entry) - return [nil, false] if account.nil? - rescue Goldfinger::Error - return [nil, false] - end - else - account = @account - end + account = @account return [nil, false] if account.suspended? From 76eda2fc21dfe16fb470a235edacbed06f608d59 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 7 Jul 2017 21:12:16 +0900 Subject: [PATCH 058/114] Add recursive object support to API response (#4095) --- config/initializers/active_model_serializers.rb | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 config/initializers/active_model_serializers.rb diff --git a/config/initializers/active_model_serializers.rb b/config/initializers/active_model_serializers.rb new file mode 100644 index 00000000000..b0230267d49 --- /dev/null +++ b/config/initializers/active_model_serializers.rb @@ -0,0 +1,3 @@ +ActiveModelSerializers.config.tap do |config| + config.default_includes = '**' +end From 91d548f7e6a0947e80be1dd4d4341b1d99c461e4 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Fri, 7 Jul 2017 21:12:45 +0900 Subject: [PATCH 059/114] Update webpack-dev-server to v2.5.1 (#4094) --- package.json | 2 +- yarn.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 49d9f0fdb4f..2f63d0bbdfc 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "react-intl-translations-manager": "^5.0.0", "react-test-renderer": "^15.6.1", "sinon": "^2.3.5", - "webpack-dev-server": "webpack/webpack-dev-server#047a5954398e67b0a17e6be7d5877d9f55f40cba", + "webpack-dev-server": "^2.5.1", "yargs": "^8.0.2" }, "optionalDependencies": { diff --git a/yarn.lock b/yarn.lock index 4a909d134a6..d8ed20343d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7198,9 +7198,9 @@ webpack-dev-middleware@^1.10.2, webpack-dev-middleware@^1.11.0: path-is-absolute "^1.0.0" range-parser "^1.0.3" -webpack-dev-server@webpack/webpack-dev-server#047a5954398e67b0a17e6be7d5877d9f55f40cba: - version "2.5.0" - resolved "https://codeload.github.com/webpack/webpack-dev-server/tar.gz/047a5954398e67b0a17e6be7d5877d9f55f40cba" +webpack-dev-server@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.5.1.tgz#a02e726a87bb603db5d71abb7d6d2649bf10c769" dependencies: ansi-html "0.0.7" bonjour "^3.5.0" From 6e1261f277411300e4f1d24f8c1b14ad1a165915 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 7 Jul 2017 16:19:28 +0200 Subject: [PATCH 060/114] Fix notifications including wrong status in JSON (#4097) --- app/serializers/rest/notification_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/serializers/rest/notification_serializer.rb b/app/serializers/rest/notification_serializer.rb index 97fadf32edf..f95d099a319 100644 --- a/app/serializers/rest/notification_serializer.rb +++ b/app/serializers/rest/notification_serializer.rb @@ -4,7 +4,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer attributes :id, :type, :created_at belongs_to :from_account, key: :account, serializer: REST::AccountSerializer - belongs_to :status, if: :status_type?, serializer: REST::StatusSerializer + belongs_to :target_status, key: :status, if: :status_type?, serializer: REST::StatusSerializer def status_type? [:favourite, :reblog, :mention].include?(object.type) From ebd2dde688b61b3987dc8e209921d964d10a33de Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 7 Jul 2017 16:56:52 +0200 Subject: [PATCH 061/114] Restore streaming API output format (#4100) * Restore streaming API output format Regression from #4090 * Remove whitespace --- streaming/index.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/streaming/index.js b/streaming/index.js index 400456d2433..6ee8baea87f 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -262,11 +262,12 @@ const startWorker = (workerId) => { const { event, payload, queued_at } = JSON.parse(message); const transmit = () => { - const now = new Date().getTime(); - const delta = now - queued_at; + const now = new Date().getTime(); + const delta = now - queued_at; + const encodedPayload = typeof payload === 'number' ? payload : JSON.stringify(payload); - log.silly(req.requestId, `Transmitting for ${req.accountId}: ${event} ${payload} Delay: ${delta}ms`); - output(event, payload); + log.silly(req.requestId, `Transmitting for ${req.accountId}: ${event} ${encodedPayload} Delay: ${delta}ms`); + output(event, encodedPayload); }; if (notificationOnly && event !== 'notification') { @@ -282,7 +283,7 @@ const startWorker = (workerId) => { return; } - const unpackedPayload = JSON.parse(payload); + const unpackedPayload = payload; const targetAccountIds = [unpackedPayload.account.id].concat(unpackedPayload.mentions.map(item => item.id)); const accountDomain = unpackedPayload.account.acct.split('@')[1]; From 9fe6cfca48b005158979583e2266d7899e84dbe0 Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Fri, 7 Jul 2017 17:35:47 +0200 Subject: [PATCH 062/114] i18n: @18d3fa9 (pl) (#4101) --- config/locales/simple_form.pl.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index 09e76eba09a..2a3756d4d39 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -43,6 +43,7 @@ pl: setting_boost_modal: Pytaj o potwierdzenie przed podbiciem setting_default_privacy: Widoczność posta setting_delete_modal: Pytaj o potwierdzenie przed usunięciem postu + setting_system_font_ui: Używaj domyślnej czcionki systemu severity: Priorytet type: Typ importu username: Nazwa użytkownika From 3f82d8b979104c7056fe431a9c2bd32e1004c9c5 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Fri, 7 Jul 2017 11:01:00 -0700 Subject: [PATCH 063/114] Gracefully stop streaming server (#4103) --- streaming/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/streaming/index.js b/streaming/index.js index 6ee8baea87f..c7e0de96c2f 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -467,6 +467,7 @@ const startWorker = (workerId) => { const onExit = () => { log.info(`Worker ${workerId} exiting, bye bye`); server.close(); + process.exit(0); }; const onError = (err) => { From 7a549f830e0d77af3020243617c5ab8bd811fd8d Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Fri, 7 Jul 2017 20:01:17 +0200 Subject: [PATCH 064/114] i18n: improve consistency (pl) (#4104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcin Mikołajczak --- config/locales/pl.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/config/locales/pl.yml b/config/locales/pl.yml index f0546dd0ce6..bf9d5e034b6 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -61,8 +61,8 @@ pl: edit: Edytuj email: Adres e-mail feed_url: Adres kanału - followers: Obserwujący - follows: Obserwacje + followers: Śledzący + follows: Śledzeni ip: Adres IP location: all: Wszystkie @@ -114,7 +114,7 @@ pl: create: Utwórz blokadę hint: Blokada domen nie zabroni tworzenia wpisów kont w bazie danych, ale pozwoli na automatyczną moderację kont do nich należących. severity: - desc_html: "Wyciszenie uczyni wpisy użytkownika niewidoczne dla osób, które go nie obserwują. Zawieszenie spowoduje usunięcie całej zawartości dodanej przez użytkownika." + desc_html: "Wyciszenie uczyni wpisy użytkownika widoczne tylko dla osób, które go śledzą. Zawieszenie spowoduje usunięcie całej zawartości dodanej przez użytkownika." silence: Wycisz suspend: Zawieś title: Nowa blokada domen @@ -262,15 +262,15 @@ pl: storage: Urządzenie przechowujące dane followers: domain: Domena - explanation_html: Jeżeli chcesz mieć pewność, kto może przeczytać Twoje statusy, musisz kontrolować, kto Cię obserwuje. Twoje prywatne statusy są dostarczane na te instancje, na których jesteś obserwowany. Możesz sprawdzać swoich obserwowanych i blokować ich, jeśli nie ufasz właścicielom lub oprogramowaniu danej instancji. - followers_count: Liczba obserwujących + explanation_html: Jeżeli chcesz mieć pewność, kto może przeczytać Twoje statusy, musisz kontrolować, kto śledzi Twój profil. Twoje prywatne statusy są dostarczane na te instancje, na których jesteś śledzony. Możesz sprawdzać, kto Cię śledzi i blokować ich, jeśli nie ufasz właścicielom lub oprogramowaniu danej instancji. + followers_count: Liczba śledzących lock_link: Zablokuj swoje konto - purge: Usuń z obserwujących + purge: Przestań śledzić success: - one: W trakcie usuwania obserwujcych z jednej domeny… - other: W trakcie usuwania obserwujących z %{count} domen… + one: W trakcie usuwania śledzących z jednej domeny… + other: W trakcie usuwania śledzących z %{count} domen… true_privacy_html: Pamiętaj, że rzeczywista prywatność może zostać uzyskana wyłącznie dzięki szyfrowaniu end-to-end. - unlocked_warning_html: Każdy może cię zaobserwować, aby natychmiastowo zobaczyć twoje statusy. %{lock_link} aby móc kontrolować obserwujących. + unlocked_warning_html: Każdy może cię śledzić, aby natychmiastowo zobaczyć twoje statusy. %{lock_link} aby móc kontrolować, kto Cię śledzi. unlocked_warning_title: Twoje konto nie jest zablokowane generic: changes_saved_msg: Ustawienia zapisane! @@ -374,7 +374,7 @@ pl: delete: Usuń konto edit_profile: Edytuj profil export: Eksportuj dane - followers: Autoryzowani obserwujący + followers: Autoryzowani śledzący import: Importuj dane preferences: Preferencje settings: Ustawienia @@ -385,7 +385,7 @@ pl: show_more: Pokaż więcej visibilities: private: Tylko dla śledzących - private_long: Widoczne tylko obserwowanych + private_long: Widoczne tylko dla śledzących public: Publiczny public_long: Widoczne dla wszystkich unlisted: Niewypisany From 00df69bc89f1b5ffdf290bde8359b3854e2b1395 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 7 Jul 2017 23:25:15 +0200 Subject: [PATCH 065/114] Fix #4058 - Use a long-lived cookie to keep track of user-level sessions (#4091) * Fix #4058 - Use a long-lived cookie to keep track of user-level sessions * Fix tests, smooth migrate from previous session-based identifier --- app/controllers/application_controller.rb | 2 +- config/initializers/devise.rb | 20 ++++++++++++++++---- spec/rails_helper.rb | 11 ++++++++--- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 865fcd12576..b3c2db02b34 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -70,7 +70,7 @@ class ApplicationController < ActionController::Base end 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 def cache_collection(raw, klass) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index d51471d308a..bf61ea0ea34 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,17 +1,29 @@ Warden::Manager.after_set_user except: :fetch do |user, warden| - SessionActivation.deactivate warden.raw_session['auth_id'] - warden.raw_session['auth_id'] = user.activate_session(warden.request) + SessionActivation.deactivate warden.cookies.signed['_session_id'] + + warden.cookies.signed['_session_id'] = { + value: user.activate_session(warden.request), + expires: 1.year.from_now, + httponly: true, + } end Warden::Manager.after_fetch do |user, warden| - unless user.session_active?(warden.raw_session['auth_id']) + if user.session_active?(warden.cookies.signed['_session_id'] || warden.raw_session['auth_id']) + warden.cookies.signed['_session_id'] = { + value: warden.cookies.signed['_session_id'] || warden.raw_session['auth_id'], + expires: 1.year.from_now, + httponly: true, + } + else warden.logout throw :warden, message: :unauthenticated end end Warden::Manager.before_logout do |_, warden| - SessionActivation.deactivate warden.raw_session['auth_id'] + SessionActivation.deactivate warden.cookies.signed['_session_id'] + warden.cookies.delete('_session_id') end Devise.setup do |config| diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 9a4c8fd3c9f..4f7399505c4 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -20,11 +20,16 @@ Sidekiq::Logging.logger = nil Devise::Test::ControllerHelpers.module_eval do alias_method :original_sign_in, :sign_in - def sign_in(resource, deprecated = nil, scope: nil) + def sign_in(resource, _deprecated = nil, scope: nil) original_sign_in(resource, scope: scope) - SessionActivation.deactivate warden.raw_session["auth_id"] - warden.raw_session["auth_id"] = resource.activate_session(warden.request) + SessionActivation.deactivate warden.cookies.signed['_session_id'] + + warden.cookies.signed['_session_id'] = { + value: resource.activate_session(warden.request), + expires: 1.year.from_now, + httponly: true, + } end end From 348d6f5e7551e632e7dea41e61c40f79aac59be9 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Sat, 8 Jul 2017 00:06:02 +0200 Subject: [PATCH 066/114] Lazy load components (#3879) * feat: Lazy-load routes * feat: Lazy-load modals * feat: Lazy-load columns * refactor: Simplify Bundle API * feat: Optimize bundles * feat: Prevent flashing the waiting state * feat: Preload commonly used bundles * feat: Lazy load Compose reducers * feat: Lazy load Notifications reducer * refactor: Move all dynamic imports into one file * fix: Minor bugs * fix: Manually hydrate the lazy-loaded reducers * refactor: Move all dynamic imports to async-components * fix: Loading modal style * refactor: Avoid converting the raw state for each lazy hydration * refactor: Remove unused component * refactor: Maintain modal name * fix: Add as=script to preload link * chore: Fix lint error * fix(components/bundle): Check if timestamp is set when computing elapsed * fix: Load compose reducers for the onboarding modal --- app/javascript/mastodon/actions/bundles.js | 25 +++ app/javascript/mastodon/actions/store.js | 8 + app/javascript/mastodon/components/status.js | 27 +++- .../mastodon/containers/mastodon.js | 5 +- .../components/emoji_picker_dropdown.js | 3 +- .../mastodon/features/ui/components/bundle.js | 96 ++++++++++++ .../ui/components/bundle_column_error.js | 44 ++++++ .../ui/components/bundle_modal_error.js | 53 +++++++ .../features/ui/components/column_loading.js | 13 ++ .../features/ui/components/columns_area.js | 28 +++- .../features/ui/components/modal_loading.js | 20 +++ .../features/ui/components/modal_root.js | 51 ++++--- .../ui/containers/bundle_container.js | 19 +++ app/javascript/mastodon/features/ui/index.js | 87 ++++------- .../features/ui/util/async-components.js | 143 ++++++++++++++++++ .../features/ui/util/react_router_helpers.js | 65 ++++++++ app/javascript/mastodon/reducers/compose.js | 4 +- app/javascript/mastodon/reducers/index.js | 21 +-- .../mastodon/reducers/media_attachments.js | 4 +- .../mastodon/store/configureStore.js | 25 ++- app/javascript/styles/components.scss | 31 +++- app/views/layouts/application.html.haml | 17 +++ 22 files changed, 679 insertions(+), 110 deletions(-) create mode 100644 app/javascript/mastodon/actions/bundles.js create mode 100644 app/javascript/mastodon/features/ui/components/bundle.js create mode 100644 app/javascript/mastodon/features/ui/components/bundle_column_error.js create mode 100644 app/javascript/mastodon/features/ui/components/bundle_modal_error.js create mode 100644 app/javascript/mastodon/features/ui/components/column_loading.js create mode 100644 app/javascript/mastodon/features/ui/components/modal_loading.js create mode 100644 app/javascript/mastodon/features/ui/containers/bundle_container.js create mode 100644 app/javascript/mastodon/features/ui/util/async-components.js create mode 100644 app/javascript/mastodon/features/ui/util/react_router_helpers.js diff --git a/app/javascript/mastodon/actions/bundles.js b/app/javascript/mastodon/actions/bundles.js new file mode 100644 index 00000000000..ecc9c8f7d3e --- /dev/null +++ b/app/javascript/mastodon/actions/bundles.js @@ -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, + }; +} diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index 601cea0014d..08c2810cae9 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -1,6 +1,7 @@ import Immutable from 'immutable'; export const STORE_HYDRATE = 'STORE_HYDRATE'; +export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; const convertState = rawState => Immutable.fromJS(rawState, (k, v) => @@ -15,3 +16,10 @@ export function hydrateStore(rawState) { state, }; }; + +export function hydrateStoreLazy(name, state) { + return { + type: `${STORE_HYDRATE_LAZY}-${name}`, + state, + }; +}; diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index ff574ab3d3b..18ce0198eb2 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -5,8 +5,6 @@ import Avatar from './avatar'; import AvatarOverlay from './avatar_overlay'; import RelativeTimestamp from './relative_timestamp'; import DisplayName from './display_name'; -import MediaGallery from './media_gallery'; -import VideoPlayer from './video_player'; import StatusContent from './status_content'; import StatusActionBar from './status_action_bar'; import { FormattedMessage } from 'react-intl'; @@ -14,6 +12,11 @@ import emojify from '../emoji'; import escapeTextContentForBrowser from 'escape-html'; import ImmutablePureComponent from 'react-immutable-pure-component'; import scheduleIdleTask from '../features/ui/util/schedule_idle_task'; +import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components'; + +// We use the component (and not the container) since we do not want +// to use the progress bar to show download progress +import Bundle from '../features/ui/components/bundle'; export default class Status extends ImmutablePureComponent { @@ -154,6 +157,14 @@ export default class Status extends ImmutablePureComponent { this.setState({ isExpanded: !this.state.isExpanded }); }; + renderLoadingMediaGallery () { + return
; + } + + renderLoadingVideoPlayer () { + return
; + } + render () { let media = null; let statusAvatar; @@ -201,9 +212,17 @@ export default class Status extends ImmutablePureComponent { if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - media = ; + media = ( + + {Component => } + + ); } else { - media = ; + media = ( + + {Component => } + + ); } } diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index 3bd89902f67..6e79f9e4f7f 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -22,9 +22,10 @@ import { getLocale } from '../locales'; const { localeData, messages } = getLocale(); addLocaleData(localeData); -const store = configureStore(); +export const store = configureStore(); const initialState = JSON.parse(document.getElementById('initial-state').textContent); -store.dispatch(hydrateStore(initialState)); +export const hydrateAction = hydrateStore(initialState); +store.dispatch(hydrateAction); export default class Mastodon extends React.PureComponent { diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index c83dbb63ece..83c66a5d594 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -2,6 +2,7 @@ import React from 'react'; import Dropdown, { DropdownTrigger, DropdownContent } from 'react-simple-dropdown'; import PropTypes from 'prop-types'; import { defineMessages, injectIntl } from 'react-intl'; +import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -50,7 +51,7 @@ export default class EmojiPickerDropdown extends React.PureComponent { this.setState({ active: true }); if (!EmojiPicker) { this.setState({ loading: true }); - import(/* webpackChunkName: "emojione_picker" */ 'emojione-picker').then(TheEmojiPicker => { + EmojiPickerAsync().then(TheEmojiPicker => { EmojiPicker = TheEmojiPicker.default; this.setState({ loading: false }); }).catch(() => { diff --git a/app/javascript/mastodon/features/ui/components/bundle.js b/app/javascript/mastodon/features/ui/components/bundle.js new file mode 100644 index 00000000000..e69a32f474c --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle.js @@ -0,0 +1,96 @@ +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, + onRender: PropTypes.func, + onFetch: PropTypes.func, + onFetchSuccess: PropTypes.func, + onFetchFail: PropTypes.func, + } + + static defaultProps = { + loading: emptyComponent, + error: emptyComponent, + renderDelay: 0, + onRender: noop, + onFetch: noop, + onFetchSuccess: noop, + onFetchFail: noop, + } + + state = { + mod: undefined, + forceRender: false, + } + + componentWillMount() { + this.load(this.props); + } + + componentWillReceiveProps(nextProps) { + if (nextProps.fetchComponent !== this.props.fetchComponent) { + this.load(nextProps); + } + } + + componentDidUpdate () { + this.props.onRender(); + } + + 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); + } + + return fetchComponent() + .then((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) ? : null; + } + + if (mod === null) { + return ; + } + + return children(mod); + } + +} + +export default Bundle; diff --git a/app/javascript/mastodon/features/ui/components/bundle_column_error.js b/app/javascript/mastodon/features/ui/components/bundle_column_error.js new file mode 100644 index 00000000000..cd124746acb --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle_column_error.js @@ -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 ( + + + +
+ + {formatMessage(messages.body)} +
+
+ ); + } + +} + +export default injectIntl(BundleColumnError); diff --git a/app/javascript/mastodon/features/ui/components/bundle_modal_error.js b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js new file mode 100644 index 00000000000..928bfe1f7d5 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/bundle_modal_error.js @@ -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 + // (make sure they have the same dimensions) + return ( +
+
+ + {formatMessage(messages.error)} +
+ +
+
+ +
+
+
+ ); + } + +} + +export default injectIntl(BundleModalError); diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.js new file mode 100644 index 00000000000..9bb9c14a103 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/column_loading.js @@ -0,0 +1,13 @@ +import React from 'react'; + +import Column from '../../../components/column'; +import ColumnHeader from '../../../components/column_header'; + +const ColumnLoading = () => ( + + +
+ +); + +export default ColumnLoading; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 01167b6e5d2..5fa27599fba 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -2,15 +2,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; + import ReactSwipeable from 'react-swipeable'; -import HomeTimeline from '../../home_timeline'; -import Notifications from '../../notifications'; -import PublicTimeline from '../../public_timeline'; -import CommunityTimeline from '../../community_timeline'; -import HashtagTimeline from '../../hashtag_timeline'; -import Compose from '../../compose'; import { getPreviousLink, getNextLink } from './tabs_bar'; +import BundleContainer from '../containers/bundle_container'; +import ColumnLoading from './column_loading'; +import BundleColumnError from './bundle_column_error'; +import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline } from '../../ui/util/async-components'; + const componentMap = { 'COMPOSE': Compose, 'HOME': HomeTimeline, @@ -48,6 +48,14 @@ export default class ColumnsArea extends ImmutablePureComponent { } }; + renderLoading = () => { + return ; + } + + renderError = (props) => { + return ; + } + render () { const { columns, children, singleColumn } = this.props; @@ -62,9 +70,13 @@ export default class ColumnsArea extends ImmutablePureComponent { return (
{columns.map(column => { - const SpecificComponent = componentMap[column.get('id')]; const params = column.get('params', null) === null ? null : column.get('params').toJS(); - return ; + + return ( + + {SpecificComponent => } + + ); })} {React.Children.map(children, child => React.cloneElement(child, { multiColumn: true }))} diff --git a/app/javascript/mastodon/features/ui/components/modal_loading.js b/app/javascript/mastodon/features/ui/components/modal_loading.js new file mode 100644 index 00000000000..f403ca4c9e9 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/modal_loading.js @@ -0,0 +1,20 @@ +import React from 'react'; + +import LoadingIndicator from '../../../components/loading_indicator'; + +// Keep the markup in sync with +// (make sure they have the same dimensions) +const ModalLoading = () => ( +
+
+ +
+
+
+
+
+
+); + +export default ModalLoading; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 48b048eb7e3..085299038ac 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -1,13 +1,18 @@ import React from 'react'; 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 TransitionMotion from 'react-motion/lib/TransitionMotion'; 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, +} from '../../../features/ui/util/async-components'; const MODAL_COMPONENTS = { 'MEDIA': MediaModal, @@ -49,6 +54,22 @@ export default class ModalRoot extends React.PureComponent { return { opacity: spring(0), scale: spring(0.98) }; } + renderModal = (SpecificComponent) => { + const { props, onClose } = this.props; + + return ; + } + + renderLoading = () => { + return ; + } + + renderError = (props) => { + const { onClose } = this.props; + + return ; + } + render () { const { type, props, onClose } = this.props; const visible = !!type; @@ -70,18 +91,14 @@ export default class ModalRoot extends React.PureComponent { > {interpolatedStyles =>
- {interpolatedStyles.map(({ key, data: { type, props }, style }) => { - const SpecificComponent = MODAL_COMPONENTS[type]; - - return ( -
-
-
- -
+ {interpolatedStyles.map(({ key, data: { type }, style }) => ( +
+
+
+ {this.renderModal}
- ); - })} +
+ ))}
} diff --git a/app/javascript/mastodon/features/ui/containers/bundle_container.js b/app/javascript/mastodon/features/ui/containers/bundle_container.js new file mode 100644 index 00000000000..7e3f0c3a6bb --- /dev/null +++ b/app/javascript/mastodon/features/ui/containers/bundle_container.js @@ -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); diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 54e623d9952..6057d87970d 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -1,7 +1,5 @@ import React from 'react'; import classNames from 'classnames'; -import Switch from 'react-router-dom/Switch'; -import Route from 'react-router-dom/Route'; import Redirect from 'react-router-dom/Redirect'; import NotificationsContainer from './containers/notifications_container'; import PropTypes from 'prop-types'; @@ -14,64 +12,40 @@ import { debounce } from 'lodash'; import { uploadCompose } from '../../actions/compose'; import { refreshHomeTimeline } from '../../actions/timelines'; import { refreshNotifications } from '../../actions/notifications'; +import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; +import { store } from '../../containers/mastodon'; import ColumnsAreaContainer from './containers/columns_area_container'; -import Status from '../../features/status'; -import GettingStarted from '../../features/getting_started'; -import PublicTimeline from '../../features/public_timeline'; -import CommunityTimeline from '../../features/community_timeline'; -import AccountTimeline from '../../features/account_timeline'; -import AccountGallery from '../../features/account_gallery'; -import HomeTimeline from '../../features/home_timeline'; -import Compose from '../../features/compose'; -import Followers from '../../features/followers'; -import Following from '../../features/following'; -import Reblogs from '../../features/reblogs'; -import Favourites from '../../features/favourites'; -import HashtagTimeline from '../../features/hashtag_timeline'; -import Notifications from '../../features/notifications'; -import FollowRequests from '../../features/follow_requests'; -import GenericNotFound from '../../features/generic_not_found'; -import FavouritedStatuses from '../../features/favourited_statuses'; -import Blocks from '../../features/blocks'; -import Mutes from '../../features/mutes'; +import { + Compose, + Status, + GettingStarted, + PublicTimeline, + CommunityTimeline, + AccountTimeline, + AccountGallery, + HomeTimeline, + Followers, + Following, + Reblogs, + Favourites, + HashtagTimeline, + Notifications as AsyncNotifications, + FollowRequests, + GenericNotFound, + FavouritedStatuses, + Blocks, + Mutes, +} from './util/async-components'; -// Small wrapper to pass multiColumn to the route components -const WrappedSwitch = ({ multiColumn, children }) => ( - - {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} - -); +const Notifications = () => AsyncNotifications().then(component => { + store.dispatch(refreshNotifications()); + return component; +}); -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 {content}; - } - - render () { - const { component: Component, content, ...rest } = this.props; - - return ; - } - -} +// Dummy import, to make sure that ends up in the application bundle. +// Without this it ends up in ~8 very commonly used bundles. +import '../../components/status'; const mapStateToProps = state => ({ systemFontUi: state.getIn(['meta', 'system_font_ui']), @@ -162,7 +136,6 @@ export default class UI extends React.PureComponent { document.addEventListener('dragend', this.handleDragEnd, false); this.props.dispatch(refreshHomeTimeline()); - this.props.dispatch(refreshNotifications()); } componentWillUnmount () { diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js new file mode 100644 index 00000000000..c9f81136d08 --- /dev/null +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -0,0 +1,143 @@ +import { store } from '../../../containers/mastodon'; +import { injectAsyncReducer } from '../../../store/configureStore'; + +// NOTE: When lazy-loading reducers, make sure to add them +// to application.html.haml (if the component is preloaded there) + +export function EmojiPicker () { + return import(/* webpackChunkName: "emojione_picker" */'emojione-picker'); +} + +export function Compose () { + return Promise.all([ + import(/* webpackChunkName: "features/compose" */'../../compose'), + import(/* webpackChunkName: "reducers/compose" */'../../../reducers/compose'), + import(/* webpackChunkName: "reducers/media_attachments" */'../../../reducers/media_attachments'), + import(/* webpackChunkName: "reducers/search" */'../../../reducers/search'), + ]).then(([component, composeReducer, mediaAttachmentsReducer, searchReducer]) => { + injectAsyncReducer(store, 'compose', composeReducer.default); + injectAsyncReducer(store, 'media_attachments', mediaAttachmentsReducer.default); + injectAsyncReducer(store, 'search', searchReducer.default); + + return component; + }); +} + +export function Notifications () { + return Promise.all([ + import(/* webpackChunkName: "features/notifications" */'../../notifications'), + import(/* webpackChunkName: "reducers/notifications" */'../../../reducers/notifications'), + ]).then(([component, notificationsReducer]) => { + injectAsyncReducer(store, 'notifications', notificationsReducer.default); + + return component; + }); +} + +export function HomeTimeline () { + return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline'); +} + +export function PublicTimeline () { + return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline'); +} + +export function CommunityTimeline () { + return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline'); +} + +export function HashtagTimeline () { + return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); +} + +export function Status () { + return import(/* webpackChunkName: "features/status" */'../../status'); +} + +export function GettingStarted () { + return import(/* webpackChunkName: "features/getting_started" */'../../getting_started'); +} + +export function AccountTimeline () { + return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline'); +} + +export function AccountGallery () { + return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery'); +} + +export function Followers () { + return import(/* webpackChunkName: "features/followers" */'../../followers'); +} + +export function Following () { + return import(/* webpackChunkName: "features/following" */'../../following'); +} + +export function Reblogs () { + return import(/* webpackChunkName: "features/reblogs" */'../../reblogs'); +} + +export function Favourites () { + return import(/* webpackChunkName: "features/favourites" */'../../favourites'); +} + +export function FollowRequests () { + return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests'); +} + +export function GenericNotFound () { + return import(/* webpackChunkName: "features/generic_not_found" */'../../generic_not_found'); +} + +export function FavouritedStatuses () { + return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses'); +} + +export function Blocks () { + return import(/* webpackChunkName: "features/blocks" */'../../blocks'); +} + +export function Mutes () { + return import(/* webpackChunkName: "features/mutes" */'../../mutes'); +} + +export function MediaModal () { + return import(/* webpackChunkName: "modals/media_modal" */'../components/media_modal'); +} + +export function OnboardingModal () { + return Promise.all([ + import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal'), + import(/* webpackChunkName: "reducers/compose" */'../../../reducers/compose'), + import(/* webpackChunkName: "reducers/media_attachments" */'../../../reducers/media_attachments'), + ]).then(([component, composeReducer, mediaAttachmentsReducer]) => { + injectAsyncReducer(store, 'compose', composeReducer.default); + injectAsyncReducer(store, 'media_attachments', mediaAttachmentsReducer.default); + return component; + }); +} + +export function VideoModal () { + return import(/* webpackChunkName: "modals/video_modal" */'../components/video_modal'); +} + +export function BoostModal () { + return import(/* webpackChunkName: "modals/boost_modal" */'../components/boost_modal'); +} + +export function ConfirmationModal () { + return import(/* webpackChunkName: "modals/confirmation_modal" */'../components/confirmation_modal'); +} + +export function ReportModal () { + return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal'); +} + +export function MediaGallery () { + return import(/* webpackChunkName: "status/MediaGallery" */'../../../components/media_gallery'); +} + +export function VideoPlayer () { + return import(/* webpackChunkName: "status/VideoPlayer" */'../../../components/video_player'); +} diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js new file mode 100644 index 00000000000..e33a6df6f49 --- /dev/null +++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js @@ -0,0 +1,65 @@ +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 }) => ( + + {React.Children.map(children, child => React.cloneElement(child, { multiColumn }))} + +); + +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 }) => { + this.match = match; // Needed for this.renderBundle + + const { component } = this.props; + + return ( + + {this.renderBundle} + + ); + } + + renderLoading = () => { + return ; + } + + renderError = (props) => { + return ; + } + + renderBundle = (Component) => { + const { match: { params }, props: { content, multiColumn } } = this; + + return {content}; + } + + render () { + const { component: Component, content, ...rest } = this.props; + + return ; + } + +} diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index d0b47a85c2c..09db95e2d7e 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -23,7 +23,7 @@ import { COMPOSE_EMOJI_INSERT, } from '../actions/compose'; import { TIMELINE_DELETE } from '../actions/timelines'; -import { STORE_HYDRATE } from '../actions/store'; +import { STORE_HYDRATE_LAZY } from '../actions/store'; import Immutable from 'immutable'; import uuid from '../uuid'; @@ -134,7 +134,7 @@ const privacyPreference = (a, b) => { export default function compose(state = initialState, action) { switch(action.type) { - case STORE_HYDRATE: + case `${STORE_HYDRATE_LAZY}-compose`: return clearAll(state.merge(action.state.get('compose'))); case COMPOSE_MOUNT: return state.set('mounted', true); diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index be402a16b1b..79062f2f9c2 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -1,7 +1,6 @@ import { combineReducers } from 'redux-immutable'; import timelines from './timelines'; import meta from './meta'; -import compose from './compose'; import alerts from './alerts'; import { loadingBarReducer } from 'react-redux-loading-bar'; import modal from './modal'; @@ -9,20 +8,16 @@ import user_lists from './user_lists'; import accounts from './accounts'; import accounts_counters from './accounts_counters'; import statuses from './statuses'; -import media_attachments from './media_attachments'; import relationships from './relationships'; -import search from './search'; -import notifications from './notifications'; import settings from './settings'; import status_lists from './status_lists'; import cards from './cards'; import reports from './reports'; import contexts from './contexts'; -export default combineReducers({ +const reducers = { timelines, meta, - compose, alerts, loadingBar: loadingBarReducer, modal, @@ -30,13 +25,19 @@ export default combineReducers({ status_lists, accounts, accounts_counters, - media_attachments, statuses, relationships, - search, - notifications, settings, cards, reports, contexts, -}); +}; + +export function createReducer(asyncReducers) { + return combineReducers({ + ...reducers, + ...asyncReducers, + }); +} + +export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/media_attachments.js b/app/javascript/mastodon/reducers/media_attachments.js index 85bea4f0b38..d17d465aa49 100644 --- a/app/javascript/mastodon/reducers/media_attachments.js +++ b/app/javascript/mastodon/reducers/media_attachments.js @@ -1,4 +1,4 @@ -import { STORE_HYDRATE } from '../actions/store'; +import { STORE_HYDRATE_LAZY } from '../actions/store'; import Immutable from 'immutable'; const initialState = Immutable.Map({ @@ -7,7 +7,7 @@ const initialState = Immutable.Map({ export default function meta(state = initialState, action) { switch(action.type) { - case STORE_HYDRATE: + case `${STORE_HYDRATE_LAZY}-media_attachments`: return state.merge(action.state.get('media_attachments')); default: return state; diff --git a/app/javascript/mastodon/store/configureStore.js b/app/javascript/mastodon/store/configureStore.js index 1376d4cbaf5..0fe29f031a2 100644 --- a/app/javascript/mastodon/store/configureStore.js +++ b/app/javascript/mastodon/store/configureStore.js @@ -1,15 +1,36 @@ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; -import appReducer from '../reducers'; +import appReducer, { createReducer } from '../reducers'; +import { hydrateStoreLazy } from '../actions/store'; +import { hydrateAction } from '../containers/mastodon'; import loadingBarMiddleware from '../middleware/loading_bar'; import errorsMiddleware from '../middleware/errors'; import soundsMiddleware from '../middleware/sounds'; export default function configureStore() { - return createStore(appReducer, compose(applyMiddleware( + const store = createStore(appReducer, compose(applyMiddleware( thunk, loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), errorsMiddleware(), soundsMiddleware() ), window.devToolsExtension ? window.devToolsExtension() : f => f)); + + store.asyncReducers = { }; + + return store; }; + +export function injectAsyncReducer(store, name, asyncReducer) { + if (!store.asyncReducers[name]) { + // Keep track that we injected this reducer + store.asyncReducers[name] = asyncReducer; + + // Add the current reducer to the store + store.replaceReducer(createReducer(store.asyncReducers)); + + // The state this reducer handles defaults to its initial state (stored inside the reducer) + // But that state may be out of date because of the server-side hydration, so we replay + // the hydration action but only for this reducer (all async reducers must listen for this dynamic action) + store.dispatch(hydrateStoreLazy(name, hydrateAction.state)); + } +} diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index a87aa5d79bf..9b500c7ad99 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -2300,7 +2300,8 @@ button.icon-button.active i.fa-retweet { vertical-align: middle; } -.empty-column-indicator { +.empty-column-indicator, +.error-column { color: lighten($ui-base-color, 20%); background: $ui-base-color; text-align: center; @@ -2326,6 +2327,10 @@ button.icon-button.active i.fa-retweet { } } +.error-column { + flex-direction: column; +} + @keyframes pulse { 0% { opacity: 1; @@ -2909,7 +2914,8 @@ button.icon-button.active i.fa-retweet { z-index: 100; } -.onboarding-modal { +.onboarding-modal, +.error-modal { background: $ui-secondary-color; color: $ui-base-color; border-radius: 8px; @@ -2918,7 +2924,8 @@ button.icon-button.active i.fa-retweet { flex-direction: column; } -.onboarding-modal__pager { +.onboarding-modal__pager, +.error-modal__body { height: 80vh; width: 80vw; max-width: 520px; @@ -2943,6 +2950,14 @@ button.icon-button.active i.fa-retweet { } } +.error-modal__body { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; +} + @media screen and (max-width: 550px) { .onboarding-modal { width: 100%; @@ -2959,7 +2974,8 @@ button.icon-button.active i.fa-retweet { } } -.onboarding-modal__paginator { +.onboarding-modal__paginator, +.error-modal__footer { flex: 0 0 auto; background: darken($ui-secondary-color, 8%); display: flex; @@ -2969,7 +2985,8 @@ button.icon-button.active i.fa-retweet { min-width: 33px; } - .onboarding-modal__nav { + .onboarding-modal__nav, + .error-modal__nav { color: darken($ui-secondary-color, 34%); background-color: transparent; border: 0; @@ -2992,6 +3009,10 @@ button.icon-button.active i.fa-retweet { } } +.error-modal__footer { + justify-content: center; +} + .onboarding-modal__dots { flex: 1 1 auto; display: flex; diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index f991bc74f57..68d3468590d 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -20,6 +20,23 @@ = stylesheet_pack_tag 'application', media: 'all' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' + + = javascript_pack_tag 'features/getting_started', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'reducers/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'reducers/media_attachments', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'reducers/search', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/home_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag 'reducers/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/community_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + + = javascript_pack_tag 'features/public_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' + = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' = csrf_meta_tags From 8fecd8010801c17d0d086fbb27d4d9a67ccbb6af Mon Sep 17 00:00:00 2001 From: Sylvhem Date: Sat, 8 Jul 2017 01:27:22 +0200 Subject: [PATCH 067/114] Various fixes in the French translation (#4107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Changement de « Changement de mot de passe » en « Sécurité » * Suppression de « (Two-factor auth) » Change la valeur de la chaîne « two_factor_authentication » de « Identification à deux facteurs (Two-factor auth) » à « Identification à deux facteurs ». La traduction anglaise entre parathentèse était redondante et gênait la lecture. Change the value of the "two_factor_authentication" from "Identification à deux facteurs (Two-factor auth)" to "Identification à deux facteurs". The English translation in brackets was superflous and was getting in the way of the reader. * Remplace « ' » par « ’ » Retire de la traduction les apostrophes droites « ' » (U+0027) au profit des apostrophes typographiques « ’ » (U+2019). En typographie française, les apostrophes typographiques sont utilisées à la place des apostrophes droites. La traduction était jusqu’ici incohérente et utilisait les deux. Remove from the translation all the vertical apostrophes (U+0027) in favor of the curly ones (U+2019). In French typography, typographic apostrophes are used instead of vertical ones. The translation was incoherent and used both. * Remplace « ... » par « … » Remplace les séries de trois points par le caractère dédié « … » (U+2026). Replace all the series of three dots by the dedicated character "…" (U+2026). * Mise à jour Crée config/locales/activerecord.fr.yml, ajoute de nouvelles chaînes et met à jour certains textes. Les compteurs de caractères pour le pseudonyme et la biographie devrait maintenant pouvoir fonctionner même quand l’interface est en français. Create config/locales/activerecord.fr.yml, add new strings et update some textes. The caracters counters for the username and the biography should now work even when the interface is in French. * Remplace « A » par « À » Remplace « A » par « À » aux endroits où le mot est mal orthographié. Replace "A" by "À" when the wrong word is used. * Ajout d’espaces insécables Ajoute des espaces insécables suivant les régles nécessaires en typographie française. Add non-breaking spaces following rules of French typography. * Remplace « certain » par « certain·e » Harmonise la traduction en remplaçant « certain » par sa forme épicène. Harmonize the translation by replacing "certain" (sure) by its epicene form. * Corrige un angliscisme Remplace « adresse e-mail » par « adresse électronique ». Replace "adresse e-mail" (e-mail address) by "adresse électronique" (electronic address). --- app/javascript/mastodon/locales/fr.json | 46 +++---- .../confirmation_instructions.fr.html.erb | 4 +- .../confirmation_instructions.fr.text.erb | 6 +- .../reset_password_instructions.fr.html.erb | 6 +- .../reset_password_instructions.fr.text.erb | 6 +- config/locales/activerecord.fr.yml | 12 ++ config/locales/devise.fr.yml | 16 +-- config/locales/doorkeeper.fr.yml | 54 ++++---- config/locales/fr.yml | 130 +++++++++--------- config/locales/simple_form.fr.yml | 24 +++- 10 files changed, 163 insertions(+), 141 deletions(-) create mode 100644 config/locales/activerecord.fr.yml diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index fd2b3044423..cb7e1b5a71e 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -31,10 +31,10 @@ "column_header.unpin": "Retirer", "column_subheading.navigation": "Navigation", "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.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.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.publish": "Pouet ", "compose_form.publish_loud": "{publish}!", "compose_form.sensitive": "Marquer le média comme délicat", @@ -42,13 +42,13 @@ "compose_form.spoiler_placeholder": "Avertissement", "confirmation_modal.cancel": "Annuler", "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.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.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.message": "Confirmez vous la silenciation {name} ?", + "confirmations.mute.message": "Confirmez vous la silenciation {name} ?", "emoji_button.activity": "Activités", "emoji_button.flags": "Drapeaux", "emoji_button.food": "Boire et manger", @@ -59,20 +59,20 @@ "emoji_button.search": "Recherche…", "emoji_button.symbols": "Symboles", "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.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.public_timeline": "le fil public", "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 utilisateur⋅ice⋅s 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.reject": "Rejeter", "getting_started.appsshort": "Applications", "getting_started.faq": "FAQ", "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.userguide": "Guide d'utilisation", + "getting_started.userguide": "Guide d’utilisation", "home.column_settings.advanced": "Avancé", "home.column_settings.basic": "Basique", "home.column_settings.filter_regex": "Filtrer avec une expression rationnelle", @@ -93,17 +93,17 @@ "navigation_bar.mutes": "Comptes silencés", "navigation_bar.preferences": "Préférences", "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.mention": "{name} vous a mentionné⋅e :", - "notification.reblog": "{name} a partagé votre statut :", + "notification.mention": "{name} vous a mentionné⋅e :", + "notification.reblog": "{name} a partagé votre statut :", "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.favourite": "Favoris :", - "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :", - "notifications.column_settings.mention": "Mentions :", - "notifications.column_settings.reblog": "Partages :", + "notifications.column_settings.favourite": "Favoris :", + "notifications.column_settings.follow": "Nouveaux⋅elles abonn⋅é⋅s :", + "notifications.column_settings.mention": "Mentions :", + "notifications.column_settings.reblog": "Partages :", "notifications.column_settings.show": "Afficher dans la colonne", "notifications.column_settings.sound": "Émettre un son", "onboarding.done": "Effectué", @@ -112,18 +112,18 @@ "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_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⋅ice complet est {handle}", - "onboarding.page_one.welcome": "Bienvenue sur Mastodon !", + "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_six.admin": "L’administrateur⋅trice de votre instance est {admin}", "onboarding.page_six.almost_done": "Nous y sommes presque…", "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.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.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_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 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_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.skip": "Passer", "privacy.change": "Ajuster la confidentialité du message", @@ -151,7 +151,7 @@ "status.mute_conversation": "Masquer la conversation", "status.open": "Déplier ce statut", "status.reblog": "Partager", - "status.reblogged_by": "{name} a partagé :", + "status.reblogged_by": "{name} a partagé :", "status.reply": "Répondre", "status.replyAll": "Répondre au fil", "status.report": "Signaler @{name}", diff --git a/app/views/user_mailer/confirmation_instructions.fr.html.erb b/app/views/user_mailer/confirmation_instructions.fr.html.erb index b0b3d0f5111..fe3f0a0105e 100644 --- a/app/views/user_mailer/confirmation_instructions.fr.html.erb +++ b/app/views/user_mailer/confirmation_instructions.fr.html.erb @@ -5,10 +5,10 @@

Pour confirmer votre inscription, merci de cliquer sur le lien suivant :
<%= link_to 'Confirmer mon compte', confirmation_url(@resource, confirmation_token: @token) %>

-

Après votre première connexion, vous pourrez accéder à la documentation de l'outil.

+

Après votre première connexion, vous pourrez accéder à la documentation de l’outil.

Pensez également à jeter un œil à nos <%= link_to 'conditions d\'utilisation', terms_url %>.

Amicalement,

-

L'équipe <%= @instance %>

\ No newline at end of file +

L’équipe <%= @instance %>

diff --git a/app/views/user_mailer/confirmation_instructions.fr.text.erb b/app/views/user_mailer/confirmation_instructions.fr.text.erb index cf8e39689c1..7730715f8d8 100644 --- a/app/views/user_mailer/confirmation_instructions.fr.text.erb +++ b/app/views/user_mailer/confirmation_instructions.fr.text.erb @@ -5,10 +5,10 @@ Vous venez de vous créer un compte sur <%= @instance %> et nous vous en remerci Pour confirmer votre inscription, merci de cliquer sur le lien suivant : <%= confirmation_url(@resource, confirmation_token: @token) %> -Après votre première connexion, vous pourrez accéder à la documentation de l'outil. +Après votre première connexion, vous pourrez accéder à la documentation de l’outil. -Pour rappel, nos conditions d'utilisation sont indiquées ici <%= terms_url %> +Pour rappel, nos conditions d’utilisation sont indiquées ici <%= terms_url %> Amicalement, -L'équipe <%= @instance %> \ No newline at end of file +L’équipe <%= @instance %> diff --git a/app/views/user_mailer/reset_password_instructions.fr.html.erb b/app/views/user_mailer/reset_password_instructions.fr.html.erb index 95789e38792..db55c588480 100644 --- a/app/views/user_mailer/reset_password_instructions.fr.html.erb +++ b/app/views/user_mailer/reset_password_instructions.fr.html.erb @@ -1,8 +1,8 @@

Bonjour <%= @resource.email %> !

-

Quelqu'un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.

+

Quelqu’un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous.

<%= link_to 'Modifier mon mot de passe', edit_password_url(@resource, reset_password_token: @token) %>

-

Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message.

-

Votre mot de passe ne sera pas modifié tant que vous n'accéderez pas au lien ci-dessus et n'en choisirez pas un nouveau.

+

Si vous n’êtes pas à l’origine de cette demande, vous pouvez ignorer ce message.

+

Votre mot de passe ne sera pas modifié tant que vous n’accéderez pas au lien ci-dessus et n’en choisirez pas un nouveau.

diff --git a/app/views/user_mailer/reset_password_instructions.fr.text.erb b/app/views/user_mailer/reset_password_instructions.fr.text.erb index 73160cb4c1e..07fa3644a55 100644 --- a/app/views/user_mailer/reset_password_instructions.fr.text.erb +++ b/app/views/user_mailer/reset_password_instructions.fr.text.erb @@ -1,8 +1,8 @@ Bonjour <%= @resource.email %> ! -Quelqu'un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous. +Quelqu’un a demandé à réinitialiser votre mot de passe sur Mastodon. Vous pouvez effectuer la réinitialisation en cliquant sur le lien ci-dessous. <%= edit_password_url(@resource, reset_password_token: @token) %> -Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer ce message. -Votre mot de passe ne sera pas modifié tant que vous n'accéderez pas au lien ci-dessus et n'en choisirez pas un nouveau. +Si vous n’êtes pas à l’origine de cette demande, vous pouvez ignorer ce message. +Votre mot de passe ne sera pas modifié tant que vous n’accéderez pas au lien ci-dessus et n’en choisirez pas un nouveau. diff --git a/config/locales/activerecord.fr.yml b/config/locales/activerecord.fr.yml new file mode 100644 index 00000000000..858777c0e4c --- /dev/null +++ b/config/locales/activerecord.fr.yml @@ -0,0 +1,12 @@ +fr: + activerecord: + errors: + models: + account: + attributes: + username: + invalid: seulement des lettres, des nombres et des tirets bas + status: + attributes: + reblog: + taken: du statut existe déjà diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml index c4dbc62e0e3..6805e4f38bb 100644 --- a/config/locales/devise.fr.yml +++ b/config/locales/devise.fr.yml @@ -3,8 +3,8 @@ fr: devise: confirmations: confirmed: Votre compte a été validé. - send_instructions: Vous allez recevoir les instructions nécessaires à la confirmation de votre compte dans quelques minutes. - send_paranoid_instructions: Si votre adresse e-mail existe dans notre base de données, vous allez bientôt recevoir un courriel contenant les instructions de confirmation de votre compte. + send_instructions: Vous allez recevoir les instructions nécessaires à la confirmation de votre compte dans quelques minutes. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. + send_paranoid_instructions: Si votre adresse électronique existe dans notre base de données, vous allez bientôt recevoir un courriel contenant les instructions de confirmation de votre compte. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. failure: already_authenticated: Vous êtes déjà connecté⋅e inactive: Votre compte n’est pas encore activé. @@ -25,12 +25,12 @@ fr: unlock_instructions: subject: Instructions pour déverrouiller votre compte omniauth_callbacks: - failure: 'Nous n’avons pas pu vous authentifier via %{kind} : ''%{reason}''.' + failure: 'Nous n’avons pas pu vous authentifier via %{kind} : ''%{reason}''.' success: Authentifié avec succès via %{kind}. passwords: - no_token: Vous ne pouvez accéder à cette page sans passer par un courriel de réinitialisation de mot de passe. Si vous êtes passé⋅e par un courriel de ce type, assurez-vous d'utiliser l'URL complète. - send_instructions: Vous allez recevoir les instructions de réinitialisation du mot de passe dans quelques instants - send_paranoid_instructions: Si votre addresse e-mail existe dans notre base de données, vous allez recevoir un lien de réinitialisation par courriel + no_token: Vous ne pouvez accéder à cette page sans passer par un courriel de réinitialisation de mot de passe. Si vous êtes passé⋅e par un courriel de ce type, assurez-vous d’utiliser l’URL complète. + send_instructions: Vous allez recevoir les instructions de réinitialisation du mot de passe dans quelques instants. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. + send_paranoid_instructions: Si votre addresse électronique existe dans notre base de données, vous allez recevoir un lien de réinitialisation par courriel. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. updated: Votre mot de passe a été modifié avec succès, vous êtes maintenant connecté⋅e updated_not_active: Votre mot de passe a été modifié avec succès. registrations: @@ -46,8 +46,8 @@ fr: signed_in: Connecté. signed_out: Déconnecté. unlocks: - send_instructions: Vous allez recevoir les instructions nécessaires au déverrouillage de votre compte dans quelques instants - send_paranoid_instructions: Si votre compte existe, vous allez bientôt recevoir un courriel contenant les instructions pour le déverrouiller. + send_instructions: Vous allez recevoir les instructions nécessaires au déverrouillage de votre compte dans quelques instants. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. + send_paranoid_instructions: Si votre compte existe, vous allez bientôt recevoir un courriel contenant les instructions pour le déverrouiller. S’il vous plaît, dans le cas où vous ne recevriez pas ce message, vérifiez votre dossier d’indésirables. unlocked: Votre compte a été déverrouillé avec succès, vous êtes maintenant connecté⋅e. errors: messages: diff --git a/config/locales/doorkeeper.fr.yml b/config/locales/doorkeeper.fr.yml index 24538bc4851..0e74532c154 100644 --- a/config/locales/doorkeeper.fr.yml +++ b/config/locales/doorkeeper.fr.yml @@ -6,12 +6,12 @@ fr: remote_follow: attributes: acct: - blank: Le nom d'utilisateur ne doit pas être vide + blank: Le nom d’utilisateur ne doit pas être vide activerecord: attributes: doorkeeper/application: name: Nom - redirect_uri: L'URL de redirection + redirect_uri: L’URL de redirection errors: messages: record_invalid: Données invalides @@ -50,17 +50,17 @@ fr: edit: Modifier submit: Envoyer confirmations: - destroy: Êtes-vous certain ? + destroy: Êtes-vous certain·e ? edit: - title: Modifier l'application + title: Modifier l’application form: - error: Oups ! Vérifier votre formulaire pour des erreurs possibles + error: Oups ! Vérifier votre formulaire pour des erreurs possibles help: native_redirect_uri: Utiliser %{native_redirect_uri} pour les tests locaux redirect_uri: Utiliser une ligne par URL scopes: Séparer les portées avec des espaces. Laisser vide pour utiliser les portées par défaut. index: - callback_url: URL de retour d'appel + callback_url: URL de retour d’appel name: Nom new: Nouvelle application title: Vos applications @@ -68,11 +68,11 @@ fr: title: Nouvelle application show: actions: Actions - application_id: ID de l'application - callback_urls: URL du retour d'appel + application_id: ID de l’application + callback_urls: URL du retour d’appel scopes: Portées secret: Secret - title: 'Application : %{name}' + title: 'Application : %{name}' authorizations: buttons: authorize: Autoriser @@ -81,15 +81,15 @@ fr: title: Une erreur est survenue new: able_to: Cette application pourra - prompt: Autoriser %{client_name} à utiliser votre compte ? + prompt: Autoriser %{client_name} à utiliser votre compte ? title: Autorisation requise show: - title: Code d'autorisation + title: Code d’autorisation authorized_applications: buttons: revoke: Annuler confirmations: - revoke: Êtes-vous certain ? + revoke: Êtes-vous certain·e ? index: application: Application created_at: Créé le @@ -98,24 +98,24 @@ fr: title: Vos applications autorisées errors: messages: - access_denied: Le propriétaire de la ressource ou le serveur d'autorisation a refusé la requête. - credential_flow_not_configured: Le flux des identifiants du mot de passe du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_from_credentials n'est pas configuré. - invalid_client: L'authentification du client a échoué à cause d'un client inconnu, d'aucune authentification de client incluse ou d'une méthode d'authentification non prise en charge. - invalid_grant: Le consentement d'autorisation accordé n'est pas valide, a expiré, est annulé, ne concorde pas avec l'URL de redirection utilisée dans la requête d'autorisation ou a été émis à un autre client. - invalid_redirect_uri: L'URL de redirection n'est pas valide. + access_denied: Le propriétaire de la ressource ou le serveur d’autorisation a refusé la requête. + credential_flow_not_configured: Le flux des identifiants du mot de passe du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_from_credentials n’est pas configuré. + invalid_client: L’authentification du client a échoué à cause d’un client inconnu, d’aucune authentification de client incluse ou d’une méthode d’authentification non prise en charge. + invalid_grant: Le consentement d’autorisation accordé n’est pas valide, a expiré, est annulé, ne concorde pas avec l’URL de redirection utilisée dans la requête d’autorisation ou a été émis à un autre client. + invalid_redirect_uri: L’URL de redirection n’est pas valide. invalid_request: La requête omet un paramètre requis, inclut une valeur de paramètre non prise en charge ou est autrement mal formée. invalid_resource_owner: Les identifiants fournis par le propriétaire de la ressource ne sont pas valides ou le propriétaire de la ressource ne peut être trouvé - invalid_scope: La portée demandée n'est pas valide, est inconnue ou mal formée. + invalid_scope: La portée demandée n’est pas valide, est inconnue ou mal formée. invalid_token: - expired: Le jeton d'accès a expiré - revoked: Le jeton d'accès a été révoqué - unknown: Le jeton d'accès n'est pas valide - resource_owner_authenticator_not_configured: La recherche du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_authenticator n'est pas configuré. - server_error: Le serveur d'autorisation a rencontré une condition inattendue l'empêchant de faire aboutir la requête. - temporarily_unavailable: Le serveur d'autorisation est actuellement incapable de traiter la requête à cause d'une surcharge ou d'une maintenance temporaire du serveur. - unauthorized_client: Le client n'est pas autorisé à effectuer cette requête à l'aide de cette méthode. - unsupported_grant_type: Le type de consentement d'autorisation n'est pas pris en charge par le serveur d'autorisation. - unsupported_response_type: Le serveur d'autorisation ne prend pas en charge ce type de réponse. + expired: Le jeton d’accès a expiré + revoked: Le jeton d’accès a été révoqué + unknown: Le jeton d’accès n’est pas valide + resource_owner_authenticator_not_configured: La recherche du propriétaire de la ressource a échoué car Doorkeeper.configure.resource_owner_authenticator n’est pas configuré. + server_error: Le serveur d’autorisation a rencontré une condition inattendue l’empêchant de faire aboutir la requête. + temporarily_unavailable: Le serveur d’autorisation est actuellement incapable de traiter la requête à cause d’une surcharge ou d’une maintenance temporaire du serveur. + unauthorized_client: Le client n’est pas autorisé à effectuer cette requête à l’aide de cette méthode. + unsupported_grant_type: Le type de consentement d’autorisation n’est pas pris en charge par le serveur d’autorisation. + unsupported_response_type: Le serveur d’autorisation ne prend pas en charge ce type de réponse. flash: applications: create: diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 5a3e0c55297..fcf5f6f9e35 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -7,7 +7,7 @@ fr: business_email: Courriel professionnel closed_registrations: Les inscriptions sont actuellement fermées sur cette instance. contact: Contact - description_headline: Qu'est-ce que %{domain} ? + description_headline: Qu’est-ce que %{domain} ? domain_count_after: autres instances domain_count_before: Connectés à features: @@ -34,12 +34,12 @@ fr: follow: Suivre followers: Abonné⋅es following: Abonnements - nothing_here: Rien à voir ici ! + nothing_here: Rien à voir ici ! people_followed_by: Personnes suivies par %{name} people_who_follow: Personnes qui suivent %{name} posts: Statuts remote_follow: Suivre à distance - reserved_username: Ce nom d'utilisateur⋅ice est réservé + reserved_username: Ce nom d’utilisateur⋅ice est réservé unfollow: Ne plus suivre activitypub: activity: @@ -48,14 +48,14 @@ fr: create: name: "%{account_name} a créé une note." outbox: - name: "Boîte d'envoi de %{account_name}" - summary: Liste d'activités de %{account_name} + name: "Boîte d’envoi de %{account_name}" + summary: Liste d’activités de %{account_name} admin: accounts: - are_you_sure: Êtes-vous certain⋅e ? + are_you_sure: Êtes-vous certain⋅e ? confirm: Confirmer confirmed: Confirmé - disable_two_factor_authentication: Désactiver l'authentification à deux facteurs + disable_two_factor_authentication: Désactiver l’authentification à deux facteurs display_name: Nom affiché domain: Domaine edit: Éditer @@ -85,7 +85,7 @@ fr: perform_full_suspension: Effectuer une suspension complète profile_url: URL du profil public: Public - push_subscription_expires: Expiration de l'abonnement PuSH + push_subscription_expires: Expiration de l’abonnement PuSH redownload: Rafraîchir les avatars reset: Réinitialiser reset_password: Réinitialiser le mot de passe @@ -98,12 +98,12 @@ fr: targeted_reports: Signalements créés visant ce compte silence: Rendre muet statuses: Statuts - subscribe: S'abonner + subscribe: S’abonner title: Comptes undo_silenced: Annuler le silence undo_suspension: Annuler la suspension unsubscribe: Se désabonner - username: Nom d'utilisateur⋅ice + username: Nom d’utilisateur⋅ice web: Web domain_blocks: add_new: Ajouter @@ -112,14 +112,14 @@ fr: domain: Domaine new: create: Créer le blocage - hint: Le blocage de domaine n'empêchera pas la création de comptes dans la base de données, mais il appliquera automatiquement et rétrospectivement des méthodes de modération spécifiques sur ces comptes. + hint: Le blocage de domaine n’empêchera pas la création de comptes dans la base de données, mais il appliquera automatiquement et rétrospectivement des méthodes de modération spécifiques sur ces comptes. severity: desc_html: "Silence rendra les messages des comptes concernés invisibles à ceux qui ne les suivent pas. Suspend supprimera tout le contenu des comptes concernés, les médias, et les données du profil." silence: Muet suspend: Suspendre title: Nouveau blocage de domaine reject_media: Fichiers média rejetés - reject_media_hint: Supprime localement les fichiers média stockés et refuse d'en télécharger ultérieurement. Ne concerne pas les suspensions. + reject_media_hint: Supprime localement les fichiers média stockés et refuse d’en télécharger ultérieurement. Ne concerne pas les suspensions. severities: silence: Rendre muet suspend: Suspendre @@ -167,10 +167,10 @@ fr: contact_information: email: Entrez une adresse courriel publique label: Informations de contact - username: Entrez un nom d'utilisateur⋅ice + username: Entrez un nom d’utilisateur⋅ice registrations: closed_message: - desc_html: Affiché sur la page d'accueil lorsque les inscriptions sont fermées
Vous pouvez utiliser des balises HTML + desc_html: Affiché sur la page d’accueil lorsque les inscriptions sont fermées
Vous pouvez utiliser des balises HTML title: Message de fermeture des inscriptions open: disabled: Désactivées @@ -178,10 +178,10 @@ fr: title: Inscriptions setting: Paramètre site_description: - desc_html: Affichée sous la forme d'un paragraphe sur la page d'accueil et utilisée comme balise meta.
Vous pouvez utiliser des balises HTML, en particulier <a> et <em>. + desc_html: Affichée sous la forme d’un paragraphe sur la page d’accueil et utilisée comme balise meta.
Vous pouvez utiliser des balises HTML, en particulier <a> et <em>. title: Description du site site_description_extended: - desc_html: Affichée sur la page d'informations complémentaires du site
Vous pouvez utiliser des balises HTML + desc_html: Affichée sur la page d’informations complémentaires du site
Vous pouvez utiliser des balises HTML title: Description étendue du site site_title: Titre du site title: Paramètres du site @@ -198,17 +198,17 @@ fr: body: "%{reporter} a signalé %{target}" subject: Nouveau signalement sur %{instance} (#%{id}) application_mailer: - settings: 'Changer les préférences courriel : %{link}' + settings: 'Changer les préférences courriel : %{link}' signature: Notifications de Mastodon depuis %{instance} - view: 'Voir :' + view: 'Voir :' applications: - invalid_url: L'URL fournie est invalide + invalid_url: L’URL fournie est invalide auth: - change_password: Changer de mot de passe + change_password: Sécurité delete_account: Supprimer le compte delete_account_html: Si vous désirez supprimer votre compte, vous pouvez cliquer ici. Il vous sera demandé de confirmer cette action. - didnt_get_confirmation: Vous n’avez pas reçu les consignes de confirmation ? - forgot_password: Mot de passe oublié ? + didnt_get_confirmation: Vous n’avez pas reçu les consignes de confirmation ? + forgot_password: Mot de passe oublié ? login: Se connecter logout: Se déconnecter register: S’inscrire @@ -218,7 +218,7 @@ fr: authorize_follow: error: Malheureusement, il y a eu une erreur en cherchant les détails du compte distant follow: Suivre - prompt_html: 'Vous (%{self}) avez demandé à suivre :' + prompt_html: 'Vous (%{self}) avez demandé à suivre :' title: Suivre %{acct} datetime: distance_in_words: @@ -230,9 +230,9 @@ fr: almost_x_years: one: un an other: "%{count} ans" - half_a_minute: A l'instant + half_a_minute: À l’instant less_than_x_minutes: "%{count}min" - less_than_x_seconds: A l'instant + less_than_x_seconds: À l’instant over_x_years: one: un an other: "%{count} ans" @@ -241,19 +241,19 @@ fr: x_months: "%{count}mois" x_seconds: "%{count}s" deletes: - bad_password_msg: Bien essayé ! Mot de passe incorrect + bad_password_msg: Bien essayé ! Mot de passe incorrect confirm_password: Entrez votre mot de passe pour vérifier votre identité - description_html: Cela va supprimer votre compte et le désactiver de manière permanente et irréversible. Votre nom d'utilisateur⋅ice restera réservé afin d'éviter la confusion + description_html: Cela va supprimer votre compte et le désactiver de manière permanente et irréversible. Votre nom d’utilisateur⋅ice restera réservé afin d’éviter la confusion proceed: Supprimer compte success_msg: Votre compte a été supprimé avec succès - warning_html: Seule la suppression du contenu depuis cette instance est garantie. Le contenu qui a été partagé est susceptible de laisser des traces. Les serveurs hors-lignes ainsi que ceux n'étant plus abonnés à vos publications ne mettront pas leur base de données à jour. + warning_html: Seule la suppression du contenu depuis cette instance est garantie. Le contenu qui a été partagé est susceptible de laisser des traces. Les serveurs hors-lignes ainsi que ceux n’étant plus abonnés à vos publications ne mettront pas leur base de données à jour. warning_title: Disponibilité du contenu disséminé errors: - '403': Vous n'avez pas accès à cette page. - '404': La page que vous recherchez n'existe pas. - '410': La page que vous recherchez n'existe plus. + '403': Vous n’avez pas accès à cette page. + '404': La page que vous recherchez n’existe pas. + '410': La page que vous recherchez n’existe plus. '422': - content: Vérification de sécurité échouée. Bloquez-vous les cookies ? + content: Vérification de sécurité échouée. Bloquez-vous les cookies ? title: Vérification de sécurité échouée '429': Trop de requêtes émises dans un délai donné. noscript: Pour utiliser Mastodon, veuillez activer JavaScript @@ -265,70 +265,70 @@ fr: storage: Médias stockés followers: domain: Domaine - explanation_html: Si vous voulez être sûr⋅e que vos status restent privés, vous devez savoir qui vous suit. Vos status privés seront diffusés à toutes les instances des utilisateur⋅ice⋅s qui vous suivent. Vous voudrez peut-être les passer en revue et les supprimer si vous n'êtes pas sûr⋅e que votre vie privée sera respectée par l'administration ou le logiciel de ces instances. - followers_count: Nombre d'abonné⋅es + explanation_html: Si vous voulez être sûr⋅e que vos status restent privés, vous devez savoir qui vous suit. Vos status privés seront diffusés à toutes les instances des utilisateur⋅ice⋅s qui vous suivent. Vous voudrez peut-être les passer en revue et les supprimer si vous n’êtes pas sûr⋅e que votre vie privée sera respectée par l’administration ou le logiciel de ces instances. + followers_count: Nombre d’abonné⋅es lock_link: Rendez votre compte privé - purge: Retirer de la liste d'abonné⋅es + purge: Retirer de la liste d’abonné⋅es success: - one: Suppression des abonné⋅es venant d'un domaine en cours... - other: Suppression des abonné⋅es venant de %{count} domaines en cours... - true_privacy_html: Soyez conscient⋅es qu'une vraie confidentialité ne peut être atteinte que par un chiffrement de bout-en-bout. - unlocked_warning_html: N'importe qui peut vous suivre et voir vos status privés. %{lock_link} afin de pouvoir vérifier et rejeter des abonné⋅es. - unlocked_warning_title: Votre compte n'est pas privé + one: Suppression des abonné⋅es venant d’un domaine en cours… + other: Suppression des abonné⋅es venant de %{count} domaines en cours… + true_privacy_html: Soyez conscient⋅es qu’une vraie confidentialité ne peut être atteinte que par un chiffrement de bout-en-bout. + unlocked_warning_html: N’importe qui peut vous suivre et voir vos status privés. %{lock_link} afin de pouvoir vérifier et rejeter des abonné⋅es. + unlocked_warning_title: Votre compte n’est pas privé generic: - changes_saved_msg: Les modifications ont été enregistrées avec succès ! + changes_saved_msg: Les modifications ont été enregistrées avec succès ! powered_by: propulsé par %{link} save_changes: Enregistrer les modifications validation_errors: - one: Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous. - other: Certaines choses ne vont pas ! Vérifiez les erreurs ci-dessous. + one: Quelque chose ne va pas ! Vérifiez l’erreur ci-dessous. + other: Certaines choses ne vont pas ! Vérifiez les erreurs ci-dessous. imports: preface: Vous pouvez importer certaines données comme les personnes que vous suivez ou bloquez sur votre compte sur cette instance à partir de fichiers créés sur une autre instance. success: Vos données ont été importées avec succès et seront traitées en temps et en heure types: - blocking: Liste d'utilisateur⋅ice⋅s bloqué⋅es - following: Liste d'utilisateur⋅ice⋅s suivi⋅es - muting: Liste d'utilisateur⋅ice⋅s que vous faites taire + blocking: Liste d’utilisateur⋅ice⋅s bloqué⋅es + following: Liste d’utilisateur⋅ice⋅s suivi⋅es + muting: Liste d’utilisateur⋅ice⋅s que vous faites taire upload: Importer landing_strip_html: %{name} utilise %{link_to_root_path}. Vous pouvez le/la suivre et interagir si vous possédez un compte quelque part dans le "fediverse". - landing_strip_signup_html: Si ce n'est pas le cas, vous pouvez en créer un ici. + landing_strip_signup_html: Si ce n’est pas le cas, vous pouvez en créer un ici. media_attachments: validations: images_and_video: Impossible de joindre une vidéo à un statut contenant déjà des images too_many: Impossible de joindre plus de 4 fichiers notification_mailer: digest: - body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}) :' + body: 'Voici ce que vous avez raté sur ${instance} depuis votre dernière visite (%{}) :' mention: "%{name} vous a mentionné⋅e" new_followers_summary: - one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi ! - other: Vous avez %{count} nouveaux⋅elles abonné⋅e⋅s ! Incroyable ! + one: Vous avez un⋅e nouvel⋅le abonné⋅e ! Youpi ! + other: Vous avez %{count} nouveaux⋅elles abonné⋅e⋅s ! Incroyable ! subject: one: "Une nouvelle notification depuis votre dernière visite \U0001F418" other: "%{count} nouvelles notifications depuis votre dernière visite \U0001F418" favourite: - body: "%{name} a ajouté votre post à ses favoris :" + body: "%{name} a ajouté votre post à ses favoris :" subject: "%{name} a ajouté votre post à ses favoris" follow: - body: "%{name} vous suit !" + body: "%{name} vous suit !" subject: "%{name} vous suit" follow_request: body: "%{name} a demandé à vous suivre" - subject: 'Abonné⋅es en attente : %{name}' + subject: 'Abonné⋅es en attente : %{name}' mention: - body: "%{name} vous a mentionné⋅e dans :" + body: "%{name} vous a mentionné⋅e dans :" subject: "%{name} vous a mentionné⋅e" reblog: - body: "%{name} a partagé votre statut :" + body: "%{name} a partagé votre statut :" subject: "%{name} a partagé votre statut" pagination: next: Suivant prev: Précédent remote_follow: acct: Entrez votre pseudo@instance depuis lequel vous voulez suivre ce⋅tte utilisateur⋅trice - missing_resource: L'URL de redirection n'a pas pu être trouvée + missing_resource: L’URL de redirection n’a pas pu être trouvée proceed: Continuez pour suivre - prompt: 'Vous allez suivre :' + prompt: 'Vous allez suivre :' sessions: activity: Dernière activité browser: Navigateur @@ -376,7 +376,7 @@ fr: import: Import de données preferences: Préférences settings: Réglages - two_factor_authentication: Identification à deux facteurs (Two-factor auth) + two_factor_authentication: Identification à deux facteurs statuses: open_in_web: Ouvrir sur le web over_character_limit: limite de caractères dépassée de %{max} caractères @@ -397,20 +397,20 @@ fr: default: "%d %b %Y, %H:%M" two_factor_authentication: code_hint: Entrez le code généré par votre application pour confirmer - description_html: Si vous activez l'identification à deux facteurs, vous devrez être en possession de votre téléphone afin de générer un code de connexion. + description_html: Si vous activez l’identification à deux facteurs, vous devrez être en possession de votre téléphone afin de générer un code de connexion. disable: Désactiver enable: Activer - enabled: L'authentification à deux facteurs est activée + enabled: L’authentification à deux facteurs est activée enabled_success: Identification à deux facteurs activée avec succès generate_recovery_codes: Générer les codes de récupération instructions_html: "Scannez ce QR code grâce à Google Authenticator, Authy ou une application similaire sur votre téléphone. Désormais, cette application générera des jetons que vous devrez saisir à chaque connexion." lost_recovery_codes: Les codes de récupération vous permettent de retrouver les accès à votre comptre si vous perdez votre téléphone. Si vous perdez vos codes de récupération, vous pouvez les générer à nouveau ici. Vos anciens codes de récupération seront invalidés. - manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l''entrer manuellement, voici le secret en clair :' + manual_instructions: 'Si vous ne pouvez pas scanner ce QR code et devez l’entrer manuellement, voici le secret en clair :' recovery_codes: Codes de récupération recovery_codes_regenerated: Codes de récupération régénérés avec succès - recovery_instructions_html: Si vous perdez l'accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer l'accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants. + recovery_instructions_html: Si vous perdez l’accès à votre téléphone, vous pouvez utiliser un des codes de récupération ci-dessous pour récupérer l’accès à votre compte. Conservez les codes de récupération en toute sécurité, par exemple, en les imprimant et en les stockant avec vos autres documents importants. setup: Installer - wrong_code: Les codes entrés sont incorrects ! L'heure du serveur et celle de votre appareil sont-elles correctes ? + wrong_code: Les codes entrés sont incorrects ! L’heure du serveur et celle de votre appareil sont-elles correctes ? users: - invalid_email: L'adresse courriel est invalide - invalid_otp_token: Le code d'authentification à deux facteurs est invalide + invalid_email: L’adresse courriel est invalide + invalid_otp_token: Le code d’authentification à deux facteurs est invalide diff --git a/config/locales/simple_form.fr.yml b/config/locales/simple_form.fr.yml index 446c569473e..8717a4abd58 100644 --- a/config/locales/simple_form.fr.yml +++ b/config/locales/simple_form.fr.yml @@ -4,12 +4,20 @@ fr: hints: defaults: avatar: Au format PNG, GIF ou JPG. 2Mo maximum. Sera réduit à 120x120px - display_name: 30 caractères maximum + display_name: + one: 1 caractère restant + other: %{count} caractères restants header: Au format PNG, GIF ou JPG. 2Mo maximum. Sera réduit à 700x335px - locked: Vous devrez approuver chaque abonné⋅e et vos statuts ne s'afficheront qu'à vos abonné⋅es - note: 160 caractères maximum + locked: Vous devrez approuver chaque abonné⋅e et vos statuts ne s’afficheront qu’à vos abonné⋅es + note: + one: 1 caractère restant + other: %{count} caractères restants imports: data: Un fichier CSV généré par une autre instance de Mastodon + sessions: + otp: Entrez le code d’authentification à deux facteurs depuis votre téléphone ou utilisez un de vos codes de récupération. + user: + filtered_languages: Les langues sélectionnées seront retirées de vos fils publics. labels: defaults: avatar: Image de profil @@ -21,16 +29,18 @@ fr: email: Adresse courriel header: Image d’en-tête locale: Langue - locked: Rendre le compte privé + locked: Verrouiller le compte new_password: Nouveau mot de passe note: Présentation - otp_attempt: Code d'identification à deux facteurs + otp_attempt: Code d’identification à deux facteurs password: Mot de passe + setting_auto_play_gif: Lire automatiquement les GIFs animés setting_boost_modal: Afficher un dialogue de confirmation avant de partager setting_default_privacy: Confidentialité des statuts + setting_delete_modal: Afficher un dialogue de confirmation avant de supprimer un pouet setting_system_font_ui: Utiliser la police par défaut du système severity: Séverité - type: Type d'import + type: Type d’import username: Identifiant interactions: must_be_follower: Masquer les notifications des personnes qui ne vous suivent pas @@ -39,7 +49,7 @@ fr: digest: Envoyer des courriels récapitulatifs favourite: Envoyer un courriel lorsque quelqu’un ajoute mes statuts à ses favoris follow: Envoyer un courriel lorsque quelqu’un me suit - follow_request: Envoyer un courriel lorsque quelqu'un demande à me suivre + follow_request: Envoyer un courriel lorsque quelqu’un demande à me suivre mention: Envoyer un courriel lorsque quelqu’un me mentionne reblog: Envoyer un courriel lorsque quelqu’un partage mes statuts 'no': Non From 63b77f23202a6dece419e2eb7180395b2e276b09 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Fri, 7 Jul 2017 16:57:22 -0700 Subject: [PATCH 068/114] Avoid using getBoundingClientRect to calculate height (#4001) --- app/javascript/mastodon/components/status.js | 19 ++++++++--------- .../mastodon/components/status_content.js | 4 ---- .../mastodon/features/ui/components/bundle.js | 6 ------ .../features/ui/util/get_rect_from_entry.js | 21 +++++++++++++++++++ 4 files changed, 30 insertions(+), 20 deletions(-) create mode 100644 app/javascript/mastodon/features/ui/util/get_rect_from_entry.js diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 18ce0198eb2..df771f5a8f6 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -17,6 +17,7 @@ import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components' // We use the component (and not the container) since we do not want // to use the progress bar to show download progress import Bundle from '../features/ui/components/bundle'; +import getRectFromEntry from '../features/ui/util/get_rect_from_entry'; export default class Status extends ImmutablePureComponent { @@ -101,6 +102,11 @@ export default class Status extends ImmutablePureComponent { } handleIntersection = (entry) => { + if (this.node && this.node.children.length !== 0) { + // save the height of the fully-rendered element + this.height = getRectFromEntry(entry).height; + } + // Edge 15 doesn't support isIntersecting, but we can infer it // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12156111/ // https://github.com/WICG/IntersectionObserver/issues/211 @@ -129,15 +135,8 @@ export default class Status extends ImmutablePureComponent { this.setState((prevState) => ({ isHidden: !prevState.isIntersecting })); } - saveHeight = () => { - if (this.node && this.node.children.length !== 0) { - this.height = this.node.getBoundingClientRect().height; - } - } - handleRef = (node) => { this.node = node; - this.saveHeight(); } handleClick = () => { @@ -213,13 +212,13 @@ export default class Status extends ImmutablePureComponent { } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { media = ( - + {Component => } ); } else { media = ( - + {Component => } ); @@ -246,7 +245,7 @@ export default class Status extends ImmutablePureComponent {
- + {media} diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 78656571d11..02b4c84020c 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -17,7 +17,6 @@ export default class StatusContent extends React.PureComponent { status: ImmutablePropTypes.map.isRequired, expanded: PropTypes.bool, onExpandedToggle: PropTypes.func, - onHeightUpdate: PropTypes.func, onClick: PropTypes.func, }; @@ -56,9 +55,6 @@ export default class StatusContent extends React.PureComponent { } componentDidUpdate () { - if (this.props.onHeightUpdate) { - this.props.onHeightUpdate(); - } this._updateStatusLinks(); } diff --git a/app/javascript/mastodon/features/ui/components/bundle.js b/app/javascript/mastodon/features/ui/components/bundle.js index e69a32f474c..3eed446fec2 100644 --- a/app/javascript/mastodon/features/ui/components/bundle.js +++ b/app/javascript/mastodon/features/ui/components/bundle.js @@ -12,7 +12,6 @@ class Bundle extends React.Component { error: PropTypes.func, children: PropTypes.func.isRequired, renderDelay: PropTypes.number, - onRender: PropTypes.func, onFetch: PropTypes.func, onFetchSuccess: PropTypes.func, onFetchFail: PropTypes.func, @@ -22,7 +21,6 @@ class Bundle extends React.Component { loading: emptyComponent, error: emptyComponent, renderDelay: 0, - onRender: noop, onFetch: noop, onFetchSuccess: noop, onFetchFail: noop, @@ -43,10 +41,6 @@ class Bundle extends React.Component { } } - componentDidUpdate () { - this.props.onRender(); - } - componentWillUnmount () { if (this.timeout) { clearTimeout(this.timeout); diff --git a/app/javascript/mastodon/features/ui/util/get_rect_from_entry.js b/app/javascript/mastodon/features/ui/util/get_rect_from_entry.js new file mode 100644 index 00000000000..c266cd7dce7 --- /dev/null +++ b/app/javascript/mastodon/features/ui/util/get_rect_from_entry.js @@ -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; From 102466ac5842b20f32a9c5e2fa3f35414c34574b Mon Sep 17 00:00:00 2001 From: unarist Date: Sat, 8 Jul 2017 21:50:45 +0900 Subject: [PATCH 069/114] Fix JSON serialization of media_attachment (#4111) --- app/serializers/rest/media_attachment_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/serializers/rest/media_attachment_serializer.rb b/app/serializers/rest/media_attachment_serializer.rb index 9b07a686ef3..9055b8db478 100644 --- a/app/serializers/rest/media_attachment_serializer.rb +++ b/app/serializers/rest/media_attachment_serializer.rb @@ -15,7 +15,7 @@ class REST::MediaAttachmentSerializer < ActiveModel::Serializer end def text_url - medium_url(object.id) + object.local? ? medium_url(object) : nil end def meta From 864e3f8d9ca652e10a28bddbb0d0df629d2849d4 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 8 Jul 2017 14:51:05 +0200 Subject: [PATCH 070/114] Replace OEmbed and initial state Rabl templates with serializers (#4110) * Replace OEmbed Rabl template with serializer * Replace initial state rabl with serializer --- app/controllers/api/oembed_controller.rb | 3 +- app/controllers/home_controller.rb | 17 +++++-- app/presenters/initial_state_presenter.rb | 5 ++ app/serializers/initial_state_serializer.rb | 39 ++++++++++++++ app/serializers/oembed_serializer.rb | 56 +++++++++++++++++++++ app/views/api/oembed/show.json.rabl | 14 ------ app/views/home/index.html.haml | 2 +- app/views/home/initial_state.json.rabl | 38 -------------- spec/controllers/home_controller_spec.rb | 41 +++------------ 9 files changed, 121 insertions(+), 94 deletions(-) create mode 100644 app/presenters/initial_state_presenter.rb create mode 100644 app/serializers/initial_state_serializer.rb create mode 100644 app/serializers/oembed_serializer.rb delete mode 100644 app/views/api/oembed/show.json.rabl delete mode 100644 app/views/home/initial_state.json.rabl diff --git a/app/controllers/api/oembed_controller.rb b/app/controllers/api/oembed_controller.rb index 6e3e34d964d..f8c87dd16ee 100644 --- a/app/controllers/api/oembed_controller.rb +++ b/app/controllers/api/oembed_controller.rb @@ -5,8 +5,7 @@ class Api::OEmbedController < Api::BaseController def show @stream_entry = find_stream_entry.stream_entry - @width = maxwidth_or_default - @height = maxheight_or_default + render json: @stream_entry, serializer: OEmbedSerializer, width: maxwidth_or_default, height: maxheight_or_default end private diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 6209a3ae932..218da69066f 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -2,13 +2,10 @@ class HomeController < ApplicationController before_action :authenticate_user! + before_action :set_initial_state_json def index - @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 + @body_classes = 'app-body' end private @@ -16,4 +13,14 @@ class HomeController < ApplicationController def authenticate_user! redirect_to(single_user_mode? ? account_path(Account.first) : about_path) unless user_signed_in? end + + def set_initial_state_json + state = InitialStatePresenter.new(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)) + + serializable_resource = ActiveModelSerializers::SerializableResource.new(state, serializer: InitialStateSerializer) + @initial_state_json = serializable_resource.to_json + end end diff --git a/app/presenters/initial_state_presenter.rb b/app/presenters/initial_state_presenter.rb new file mode 100644 index 00000000000..75fef28a859 --- /dev/null +++ b/app/presenters/initial_state_presenter.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class InitialStatePresenter < ActiveModelSerializers::Model + attributes :settings, :token, :current_account, :admin +end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb new file mode 100644 index 00000000000..84f9e23a663 --- /dev/null +++ b/app/serializers/initial_state_serializer.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class InitialStateSerializer < ActiveModel::Serializer + attributes :meta, :compose, :accounts, + :media_attachments, :settings + + def meta + { + streaming_api_base_url: Rails.configuration.x.streaming_api_base_url, + access_token: object.token, + locale: I18n.locale, + domain: Rails.configuration.x.local_domain, + me: object.current_account.id, + admin: object.admin&.id, + boost_modal: object.current_account.user.setting_boost_modal, + delete_modal: object.current_account.user.setting_delete_modal, + auto_play_gif: object.current_account.user.setting_auto_play_gif, + system_font_ui: object.current_account.user.setting_system_font_ui, + } + end + + def compose + { + me: object.current_account.id, + default_privacy: object.current_account.user.setting_default_privacy, + } + end + + def accounts + store = {} + store[object.current_account.id] = ActiveModelSerializers::SerializableResource.new(object.current_account, serializer: REST::AccountSerializer) + store[object.admin.id] = ActiveModelSerializers::SerializableResource.new(object.admin, serializer: REST::AccountSerializer) unless object.admin.nil? + store + end + + def media_attachments + { accept_content_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES } + end +end diff --git a/app/serializers/oembed_serializer.rb b/app/serializers/oembed_serializer.rb new file mode 100644 index 00000000000..78376d253ee --- /dev/null +++ b/app/serializers/oembed_serializer.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class OEmbedSerializer < ActiveModel::Serializer + include RoutingHelper + include ActionView::Helpers::TagHelper + + attributes :type, :version, :title, :author_name, + :author_url, :provider_name, :provider_url, + :cache_age, :html, :width, :height + + def type + 'rich' + end + + def version + '1.0' + end + + def author_name + object.account.display_name.presence || object.account.username + end + + def author_url + account_url(object.account) + end + + def provider_name + Rails.configuration.x.local_domain + end + + def provider_url + root_url + end + + def cache_age + 86_400 + end + + def html + tag :iframe, + src: embed_account_stream_entry_url(object.account, object), + style: 'width: 100%; overflow: hidden', + frameborder: '0', + scrolling: 'no', + width: width, + height: height + end + + def width + instance_options[:width] + end + + def height + instance_options[:height] + end +end diff --git a/app/views/api/oembed/show.json.rabl b/app/views/api/oembed/show.json.rabl deleted file mode 100644 index 11dcec538c5..00000000000 --- a/app/views/api/oembed/show.json.rabl +++ /dev/null @@ -1,14 +0,0 @@ -# frozen_string_literal: true -object @stream_entry - -node(:type) { 'rich' } -node(:version) { '1.0' } -node(:title, &:title) -node(:author_name) { |entry| entry.account.display_name.blank? ? entry.account.username : entry.account.display_name } -node(:author_url) { |entry| account_url(entry.account) } -node(:provider_name) { site_hostname } -node(:provider_url) { root_url } -node(:cache_age) { 86_400 } -node(:html) { |entry| "" } -node(:width) { @width } -node(:height) { @height } diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 33c978c891c..71dcb54c63b 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,5 +1,5 @@ - content_for :header_tags do - %script#initial-state{ type: 'application/json' }!= json_escape(render(file: 'home/initial_state', formats: :json)) + %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json) = javascript_pack_tag 'application', integrity: true, crossorigin: 'anonymous' diff --git a/app/views/home/initial_state.json.rabl b/app/views/home/initial_state.json.rabl deleted file mode 100644 index c428a5a1fdd..00000000000 --- a/app/views/home/initial_state.json.rabl +++ /dev/null @@ -1,38 +0,0 @@ -object false - -node(:meta) do - { - streaming_api_base_url: @streaming_api_base_url, - access_token: @token, - locale: I18n.locale, - domain: site_hostname, - me: current_account.id, - admin: @admin.try(:id), - boost_modal: current_account.user.setting_boost_modal, - delete_modal: current_account.user.setting_delete_modal, - auto_play_gif: current_account.user.setting_auto_play_gif, - system_font_ui: current_account.user.setting_system_font_ui, - } -end - -node(:compose) do - { - me: current_account.id, - default_privacy: current_account.user.setting_default_privacy, - } -end - -node(:accounts) do - store = {} - store[current_account.id] = ActiveModelSerializers::SerializableResource.new(current_account, serializer: REST::AccountSerializer) - store[@admin.id] = ActiveModelSerializers::SerializableResource.new(@admin, serializer: REST::AccountSerializer) unless @admin.nil? - store -end - -node(:media_attachments) do - { - accept_content_types: MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES - } -end - -node(:settings) { @web_settings } diff --git a/spec/controllers/home_controller_spec.rb b/spec/controllers/home_controller_spec.rb index cc1dbe5a1ad..d44d720b146 100644 --- a/spec/controllers/home_controller_spec.rb +++ b/spec/controllers/home_controller_spec.rb @@ -23,41 +23,14 @@ RSpec.describe HomeController, type: :controller do expect(assigns(:body_classes)).to eq 'app-body' end - it 'assigns @token' do - app = Doorkeeper::Application.create!(name: 'Web', superapp: true, redirect_uri: Doorkeeper.configuration.native_redirect_uri) - allow(Doorkeeper.configuration).to receive(:access_token_expires_in).and_return(42) - + it 'assigns @initial_state_json' do subject - token = Doorkeeper::AccessToken.find_by(token: assigns(:token)) - - expect(token.application).to eq app - expect(token.resource_owner_id).to eq user.id - expect(token.scopes).to eq Doorkeeper::OAuth::Scopes.from_string('read write follow') - expect(token.expires_in_seconds).to eq 42 - expect(token.use_refresh_token?).to eq false - end - - it 'assigns @web_settings for {} if not available' do - subject - expect(assigns(:web_settings)).to eq({}) - end - - it 'assigns @web_settings for Web::Setting if available' do - setting = Fabricate('Web::Setting', data: '{"home":{}}', user: user) - subject - expect(assigns(:web_settings)).to eq setting.data - end - - it 'assigns @admin' do - admin = Fabricate(:account) - Setting.site_contact_username = admin.username - subject - expect(assigns(:admin)).to eq admin - end - - it 'assigns streaming_api_base_url' do - subject - expect(assigns(:streaming_api_base_url)).to eq 'ws://localhost:4000' + initial_state_json = json_str_to_hash(assigns(:initial_state_json)) + expect(initial_state_json[:meta]).to_not be_nil + expect(initial_state_json[:compose]).to_not be_nil + expect(initial_state_json[:accounts]).to_not be_nil + expect(initial_state_json[:settings]).to_not be_nil + expect(initial_state_json[:media_attachments]).to_not be_nil end end end From 0324f807f4f7b557bb0c38f0dbb4cfd98490957d Mon Sep 17 00:00:00 2001 From: Jeroen Date: Sat, 8 Jul 2017 17:17:02 +0200 Subject: [PATCH 071/114] Update and improvement Dutch language strings (#4117) * Update * Update --- app/javascript/mastodon/locales/nl.json | 10 +-- config/locales/nl.yml | 102 +++++++++++++++++++----- 2 files changed, 86 insertions(+), 26 deletions(-) diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 38ca6518aa7..05a9e3a12e7 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -10,7 +10,7 @@ "account.media": "Media", "account.mention": "Vermeld @{name}", "account.mute": "Negeer @{name}", - "account.posts": "Berichten", + "account.posts": "Toots", "account.report": "Rapporteer @{name}", "account.requested": "Wacht op goedkeuring", "account.unblock": "Deblokkeer @{name}", @@ -22,7 +22,7 @@ "column.community": "Lokale tijdlijn", "column.favourites": "Favorieten", "column.follow_requests": "Volgverzoeken", - "column.home": "Jouw tijdlijn", + "column.home": "Start", "column.mutes": "Genegeerde gebruikers", "column.notifications": "Meldingen", "column.public": "Globale tijdlijn", @@ -62,7 +62,7 @@ "empty_column.community": "De lokale tijdlijn is leeg. Toot iets in het openbaar om de bal aan het rollen te krijgen!", "empty_column.hashtag": "Er is nog niks te vinden onder deze hashtag.", "empty_column.home": "Jij volgt nog niemand. Bezoek {public} of gebruik het zoekvenster om andere mensen te ontmoeten.", - "empty_column.home.inactivity": "Jouw tijdlijn is leeg. Wanneer je een tijdje inactief bent geweest wordt deze snel opnieuw aangemaakt.", + "empty_column.home.inactivity": "Deze tijdlijn is leeg. Wanneer je een tijdje inactief bent geweest wordt deze snel opnieuw aangemaakt.", "empty_column.home.public_timeline": "de globale tijdlijn", "empty_column.notifications": "Je hebt nog geen meldingen. Heb interactie met andere mensen om het gesprek aan te gaan.", "empty_column.public": "Er is hier helemaal niks! Toot iets in het openbaar of volg mensen van andere Mastodon-servers om het te vullen.", @@ -109,7 +109,7 @@ "onboarding.done": "Klaar", "onboarding.next": "Volgende", "onboarding.page_five.public_timelines": "De lokale tijdlijn toont openbare toots van iedereen op {domain}. De globale tijdlijn toont openbare toots van iedereen die door gebruikers van {domain} worden gevolgd, dus ook mensen van andere Mastodon-servers. Dit zijn de openbare tijdlijnen en vormen een uitstekende manier om nieuwe mensen te ontdekken.", - "onboarding.page_four.home": "Jouw tijdlijn laat toots zien van mensen die jij volgt.", + "onboarding.page_four.home": "Deze tijdlijn laat toots zien van mensen die jij volgt.", "onboarding.page_four.notifications": "De kolom met meldingen toont alle interacties die je met andere Mastodon-gebruikers hebt.", "onboarding.page_one.federation": "Mastodon is een netwerk van onafhankelijke servers die samen een groot sociaal netwerk vormen.", "onboarding.page_one.handle": "Je bevindt je nu op {domain}, dus is jouw volledige Mastodon-adres {handle}", @@ -162,7 +162,7 @@ "status.unmute_conversation": "Conversatie niet meer negeren", "tabs_bar.compose": "Schrijven", "tabs_bar.federated_timeline": "Globaal", - "tabs_bar.home": "Jouw tijdlijn", + "tabs_bar.home": "Start", "tabs_bar.local_timeline": "Lokaal", "tabs_bar.notifications": "Meldingen", "upload_area.title": "Hierin slepen om te uploaden", diff --git a/config/locales/nl.yml b/config/locales/nl.yml index d9b02e09cd9..306ce6b1f54 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -36,10 +36,33 @@ nl: nothing_here: Hier is niets! people_followed_by: Mensen die %{name} volgt people_who_follow: Mensen die %{name} volgen - posts: Berichten + posts: Toots remote_follow: Extern volgen unfollow: Ontvolgen admin: + reports: + action_taken_by: Actie uitgevoerd door + are_you_sure: Weet je het zeker? + comment: + label: Opmerking + none: Geen + delete: Verwijderen + id: ID + mark_as_resolved: Markeer als opgelost + nsfw: + 'false': Media tonen + 'true': Media verbergen + report: 'Gerapporteerde toot #%{id}' + reported_account: Gerapporteerde account + reported_by: Gerapporteerd door + resolved: Opgelost + silence_account: Account stilzwijgen + status: Toot + suspend_account: Account blokkeren + target: Target + title: Gerapporteerde toots + unresolved: Onopgelost + view: Weergeven settings: contact_information: email: Vul een openbaar gebruikt e-mailadres in @@ -62,24 +85,11 @@ nl: title: Uitgebreide omschrijving Mastodon-server site_title: Naam Mastodon-server title: Server-instellingen - admin.reports: - comment: - label: Opmerking - none: Geen - delete: Verwijderen - id: ID - mark_as_resolved: Markeer als opgelost - report: 'Gerapporteerde toot #%{id}' - reported_account: Gerapporteerde account - reported_by: Gerapporteerd door - resolved: Opgelost - silence_account: Account stilzwijgen - status: Toot - suspend_account: Account blokkeren - target: Target - title: Gerapporteerde toots - unresolved: Onopgelost - view: Weergeven + title: Beheer + admin_mailer: + new_report: + body: "%{reporter} heeft %{target} gerapporteerd" + subject: Nieuwe toots gerapporteerd op %{instance} (#%{id}) application_mailer: settings: 'E-mailvoorkeuren wijzigen: %{link}' signature: Mastodon-meldingen van %{instance} @@ -87,7 +97,9 @@ nl: applications: invalid_url: De opgegevens URL is ongeldig auth: - change_password: Inloggegevens + change_password: Beveiliging + delete_account: Account verwijderen + delete_account_html: Wanneer je jouw account graag wilt verwijderen, kan je dat hier doen. We vragen jou daar om een bevestiging. didnt_get_confirmation: Geen bevestigingsinstructies ontvangen? forgot_password: Wachtwoord vergeten? login: Aanmelden @@ -115,12 +127,23 @@ nl: x_minutes: "%{count}m" x_months: "%{count}ma" x_seconds: "%{count}s" + deletes: + bad_password_msg: Goed geprobeerd hackers! Ongeldig wachtwoord + confirm_password: Voer jouw huidige wachtwoord in om jouw identiteit te bevestigen + description_html: Hierdoor worden alle gegevens van jouw account permanent, onomkeerbaar verwijderd en wordt deze gedeactiveerd. Om toekomstige identiteitsdiefstal te voorkomen, kan op deze server jouw gebruikersnaam niet meer gebruikt worden. + proceed: Account verwijderen + success_msg: Jouw account is succesvol verwijderd + warning_html: We kunnen alleen garanderen dat jouw gegevens op deze server worden verwijderd. Berichten (toots), incl. media, die veel zijn gedeeld laten mogelijk sporen achter. Offline servers en servers die niet meer op jouw updates zijn geabonneerd zullen niet hun databases updaten. + warning_title: Verwijdering gegevens op andere servers errors: + '403': Jij hebt geen toestemming om deze pagina te bekijken. '404': De pagina waarnaar jij op zoek bent bestaat niet. '410': De pagina waarnaar jij op zoek bent bestaat niet meer. '422': content: Veiligheidsverificatie mislukt. Blokkeer je toevallig cookies? title: Veiligheidsverificatie mislukt + '429': Te veel verbindingsaanvragen. + noscript: Schakel JavaScript in om Mastodon te kunnen gebruiken. exports: blocks: Jij blokkeert csv: CSV @@ -141,7 +164,7 @@ nl: unlocked_warning_title: Jouw account is niet besloten generic: changes_saved_msg: Wijzigingen succesvol opgeslagen! - powered_by: mogelijk gemaakt door %{link} + powered_by: wordt mogelijk gemaakt door %{link} save_changes: Wijzigingen opslaan validation_errors: one: Er is iets niet helemaal goed! Bekijk onderstaande fout @@ -189,6 +212,43 @@ nl: missing_resource: Kon vereiste doorverwijzings-URL voor jouw account niet vinden proceed: Ga door om te volgen prompt: 'Jij gaat volgen:' + sessions: + activity: Laatst actief + browser: Webbrowser + browsers: + alipay: Alipay + blackberry: Blackberry + chrome: Chrome + edge: Microsoft Edge + firefox: Firefox + generic: Onbekende webbrowser + ie: Internet Explorer + micro_messenger: MicroMessenger + nokia: Nokia S40 Ovi Browser + opera: Opera + phantom_js: PhantomJS + qq: QQ Browser + safari: Safari + uc_browser: UCBrowser + weibo: Weibo + current_session: Huidige sessie + description: "%{browser} op %{platform}" + explanation: Dit zijn de webbrowsers die momenteel met jouw Mastodon-account zijn ingelogd. + ip: IP + platforms: + adobe_air: Adobe Air + android: Android + blackberry: Blackberry + chrome_os: ChromeOS + firefox_os: Firefox OS + ios: iOS + linux: Linux + mac: Mac + other: Onbekend platform + windows: Windows + windows_mobile: Windows Mobile + windows_phone: Windows Phone + title: Sessies settings: authorized_apps: Geautoriseerde apps back: Terug naar Mastodon From 852bda3d320563400eaa74f02ef0c6c73cd8180d Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Sun, 9 Jul 2017 00:20:53 +0900 Subject: [PATCH 072/114] Use srcSet only when know width (#4112) --- app/javascript/mastodon/components/media_gallery.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 2cb1ce51c80..75222e9650a 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -91,8 +91,10 @@ class Item extends React.PureComponent { const originalUrl = attachment.get('url'); const originalWidth = attachment.getIn(['meta', 'original', 'width']); - const srcSet = `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`; - const sizes = `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`; + const hasSize = typeof originalWidth === 'number' && typeof previewWidth === 'number'; + + const srcSet = hasSize && `${originalUrl} ${originalWidth}w, ${previewUrl} ${previewWidth}w`; + const sizes = hasSize && `(min-width: 1025px) ${320 * (width / 100)}px, ${width}vw`; thumbnail = ( Date: Sun, 9 Jul 2017 00:21:59 +0900 Subject: [PATCH 073/114] Don't use preview when image size is unknown (#4113) --- .../features/ui/components/image_loader.js | 16 ++++++++++++---- .../features/ui/components/media_modal.js | 5 ++++- app/javascript/styles/components.scss | 14 ++++++++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js index 52c3a898b75..5ea55d1d26f 100644 --- a/app/javascript/mastodon/features/ui/components/image_loader.js +++ b/app/javascript/mastodon/features/ui/components/image_loader.js @@ -8,12 +8,14 @@ export default class ImageLoader extends React.PureComponent { alt: PropTypes.string, src: PropTypes.string.isRequired, previewSrc: PropTypes.string.isRequired, - width: PropTypes.number.isRequired, - height: PropTypes.number.isRequired, + width: PropTypes.number, + height: PropTypes.number, } static defaultProps = { alt: '', + width: null, + height: null, }; state = { @@ -46,8 +48,8 @@ export default class ImageLoader extends React.PureComponent { this.setState({ loading: true, error: false }); Promise.all([ this.loadPreviewCanvas(props), - this.loadOriginalImage(props), - ]) + this.hasSize() && this.loadOriginalImage(props), + ].filter(Boolean)) .then(() => { this.setState({ loading: false, error: false }); this.clearPreviewCanvas(); @@ -106,6 +108,11 @@ export default class ImageLoader extends React.PureComponent { this.removers = []; } + hasSize () { + const { width, height } = this.props; + return typeof width === 'number' && typeof height === 'number'; + } + setCanvasRef = c => { this.canvas = c; } @@ -116,6 +123,7 @@ export default class ImageLoader extends React.PureComponent { const className = classNames('image-loader', { 'image-loader--loading': loading, + 'image-loader--amorphous': !this.hasSize(), }); return ( diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index 8bb81ca01d1..a5b9dc19f30 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -74,7 +74,10 @@ export default class MediaModal extends ImmutablePureComponent { } if (attachment.get('type') === 'image') { - content = ; + const width = attachment.getIn(['meta', 'original', 'width']) || null; + const height = attachment.getIn(['meta', 'original', 'height']) || null; + + content = ; } else if (attachment.get('type') === 'gifv') { content = ; } diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 9b500c7ad99..66d2715da07 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1117,6 +1117,20 @@ height: 100%; background-image: none; } + + &.image-loader--amorphous { + position: static; + + .image-loader__preview-canvas { + display: none; + } + + .image-loader__img { + position: static; + width: auto; + height: auto; + } + } } .navigation-bar { From 46f5d3a2e95a2db7a8197929a7973259ad067a07 Mon Sep 17 00:00:00 2001 From: unarist Date: Sun, 9 Jul 2017 00:22:24 +0900 Subject: [PATCH 074/114] Fix first loading of notifications when the column is pinned (#4114) --- app/javascript/mastodon/features/ui/index.js | 9 +-------- .../mastodon/features/ui/util/async-components.js | 3 +++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 6057d87970d..8d784dfe02e 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -11,10 +11,8 @@ import { isMobile } from '../../is_mobile'; import { debounce } from 'lodash'; import { uploadCompose } from '../../actions/compose'; import { refreshHomeTimeline } from '../../actions/timelines'; -import { refreshNotifications } from '../../actions/notifications'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; -import { store } from '../../containers/mastodon'; import ColumnsAreaContainer from './containers/columns_area_container'; import { Compose, @@ -30,7 +28,7 @@ import { Reblogs, Favourites, HashtagTimeline, - Notifications as AsyncNotifications, + Notifications, FollowRequests, GenericNotFound, FavouritedStatuses, @@ -38,11 +36,6 @@ import { Mutes, } from './util/async-components'; -const Notifications = () => AsyncNotifications().then(component => { - store.dispatch(refreshNotifications()); - return component; -}); - // Dummy import, to make sure that ends up in the application bundle. // Without this it ends up in ~8 very commonly used bundles. import '../../components/status'; diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index c9f81136d08..56880dd1f65 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -1,4 +1,5 @@ import { store } from '../../../containers/mastodon'; +import { refreshNotifications } from '../../../actions/notifications'; import { injectAsyncReducer } from '../../../store/configureStore'; // NOTE: When lazy-loading reducers, make sure to add them @@ -30,6 +31,8 @@ export function Notifications () { ]).then(([component, notificationsReducer]) => { injectAsyncReducer(store, 'notifications', notificationsReducer.default); + store.dispatch(refreshNotifications()); + return component; }); } From 91cacb1e8f9b2cb20a7cda2195b91d7d85c494e9 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Sat, 8 Jul 2017 18:34:55 +0200 Subject: [PATCH 075/114] fix: Rerender Bundle on route change (#4120) --- .../features/ui/util/react_router_helpers.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/app/javascript/mastodon/features/ui/util/react_router_helpers.js b/app/javascript/mastodon/features/ui/util/react_router_helpers.js index e33a6df6f49..ede578e5600 100644 --- a/app/javascript/mastodon/features/ui/util/react_router_helpers.js +++ b/app/javascript/mastodon/features/ui/util/react_router_helpers.js @@ -31,13 +31,11 @@ export class WrappedRoute extends React.Component { } renderComponent = ({ match }) => { - this.match = match; // Needed for this.renderBundle - - const { component } = this.props; + const { component, content, multiColumn } = this.props; return ( - {this.renderBundle} + {Component => {content}} ); } @@ -50,12 +48,6 @@ export class WrappedRoute extends React.Component { return ; } - renderBundle = (Component) => { - const { match: { params }, props: { content, multiColumn } } = this; - - return {content}; - } - render () { const { component: Component, content, ...rest } = this.props; From 794781d1219112482e4abbc0a98683a17d170e2b Mon Sep 17 00:00:00 2001 From: abcang Date: Sun, 9 Jul 2017 01:35:08 +0900 Subject: [PATCH 076/114] Change account link to admin account link on report page (#4119) --- app/views/admin/reports/show.html.haml | 4 ++-- app/views/authorize_follows/_card.html.haml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index 965b7118070..44486cb4276 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -4,11 +4,11 @@ .report-accounts .report-accounts__item %strong= t('admin.reports.reported_account') - = render 'authorize_follows/card', account: @report.target_account + = render 'authorize_follows/card', account: @report.target_account, admin: true = render 'admin/accounts/card', account: @report.target_account .report-accounts__item %strong= t('admin.reports.reported_by') - = render 'authorize_follows/card', account: @report.account + = render 'authorize_follows/card', account: @report.account, admin: true = render 'admin/accounts/card', account: @report.account %p diff --git a/app/views/authorize_follows/_card.html.haml b/app/views/authorize_follows/_card.html.haml index 13d9c771975..e81e292ba60 100644 --- a/app/views/authorize_follows/_card.html.haml +++ b/app/views/authorize_follows/_card.html.haml @@ -4,7 +4,8 @@ = image_tag account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar' %span.display-name - = link_to TagManager.instance.url_for(account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do + - account_url = local_assigns[:admin] ? admin_account_path(account.id) : TagManager.instance.url_for(account) + = link_to account_url, class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'noopener' do %strong.emojify= display_name(account) %span @#{account.acct} From 007ab330e6ffb1e07995d4e306473d457043e2eb Mon Sep 17 00:00:00 2001 From: nullkal Date: Sun, 9 Jul 2017 05:44:31 +0900 Subject: [PATCH 077/114] Use charlock_holmes instead of nkf at FetchLinkCardService (#4080) * Specs for language detection * Use CharlockHolmes instead of NKF * Correct mistakes * Correct style * Set hint_enc instead of falling back and strip_tags * Improve specs * Add dependencies --- .travis.yml | 1 + Aptfile | 1 + Dockerfile | 1 + Gemfile | 1 + Gemfile.lock | 2 ++ Vagrantfile | 1 + app/services/fetch_link_card_service.rb | 8 +++++-- spec/fixtures/requests/koi8-r.txt | 20 ++++++++++++++++ spec/fixtures/requests/sjis.txt | 4 ++-- .../requests/sjis_with_wrong_charset.txt | 20 ++++++++++++++++ spec/services/fetch_link_card_service_spec.rb | 23 +++++++++++++++++++ 11 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 spec/fixtures/requests/koi8-r.txt create mode 100644 spec/fixtures/requests/sjis_with_wrong_charset.txt diff --git a/.travis.yml b/.travis.yml index 4bb33266644..4d4dc089306 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,7 @@ addons: - g++-6 - libprotobuf-dev - protobuf-compiler + - libicu-dev rvm: - 2.3.4 diff --git a/Aptfile b/Aptfile index 0456343ef48..3af0956e32b 100644 --- a/Aptfile +++ b/Aptfile @@ -3,3 +3,4 @@ libprotobuf-dev ffmpeg libxdamage1 libxfixes3 +libicu-dev diff --git a/Dockerfile b/Dockerfile index 7033cddd406..97a69139303 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,7 @@ RUN echo "@edge https://nl.alpinelinux.org/alpine/edge/main" >> /etc/apk/reposit ffmpeg \ file \ git \ + icu-dev \ imagemagick@edge \ libpq \ libxml2 \ diff --git a/Gemfile b/Gemfile index 95c74eef900..b52685cba98 100644 --- a/Gemfile +++ b/Gemfile @@ -22,6 +22,7 @@ gem 'active_model_serializers', '~> 0.10' gem 'addressable', '~> 2.5' gem 'bootsnap' gem 'browser' +gem 'charlock_holmes', '~> 0.7.3' gem 'cld3', '~> 3.1' gem 'devise', '~> 4.2' gem 'devise-two-factor', '~> 3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 71f83f73667..de0d6a10723 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -106,6 +106,7 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + charlock_holmes (0.7.3) case_transform (0.2) activesupport chunky_png (1.3.8) @@ -501,6 +502,7 @@ DEPENDENCIES capistrano-rbenv (~> 2.1) capistrano-yarn (~> 2.0) capybara (~> 2.14) + charlock_holmes (~> 0.7.3) cld3 (~> 3.1) climate_control (~> 0.2) devise (~> 4.2) diff --git a/Vagrantfile b/Vagrantfile index 1f56fcfb3fe..cbe6623b316 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -37,6 +37,7 @@ sudo apt-get install \ yarn \ libprotobuf-dev \ libreadline-dev \ + libicu-dev \ -y # Install rvm diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index 8ddaa2bf403..6ef3abb6671 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require 'nkf' class FetchLinkCardService < BaseService include HttpHelper @@ -86,7 +85,12 @@ class FetchLinkCardService < BaseService return if response.code != 200 || response.mime_type != 'text/html' html = response.to_s - page = Nokogiri::HTML(html, nil, NKF.guess(html).to_s) + + detector = CharlockHolmes::EncodingDetector.new + detector.strip_tags = true + + guess = detector.detect(html, response.charset) + page = Nokogiri::HTML(html, nil, guess&.fetch(:encoding)) card.type = :link card.title = meta_property(page, 'og:title') || page.at_xpath('//title')&.content diff --git a/spec/fixtures/requests/koi8-r.txt b/spec/fixtures/requests/koi8-r.txt new file mode 100644 index 00000000000..d4242af0196 --- /dev/null +++ b/spec/fixtures/requests/koi8-r.txt @@ -0,0 +1,20 @@ +HTTP/1.1 200 OK +Server: nginx/1.11.10 +Date: Tue, 04 Jul 2017 16:43:39 GMT +Content-Type: text/html +Content-Length: 273 +Connection: keep-alive +Last-Modified: Tue, 04 Jul 2017 16:41:34 GMT +Accept-Ranges: bytes + + + + + + XVI . . + + +

XVI . .
+

+ + diff --git a/spec/fixtures/requests/sjis.txt b/spec/fixtures/requests/sjis.txt index 9041aa25d6e..faf18d35c5e 100644 --- a/spec/fixtures/requests/sjis.txt +++ b/spec/fixtures/requests/sjis.txt @@ -11,10 +11,10 @@ Accept-Ranges: bytes - JSIS̃y[W + SJIS̃y[W -

SJIS̃y[W
+

N܂ĂLOlĂ̂̎łłBԂɈӖ҂͐ǂȔ܂܂ł\グ邽ɂ͎QlA邽Aɂ܂ȂB炢Ȃ̂͂ǂ㌎ł邾BĉcɔRKɉ]ł͂͂Ȃw}ƂoȂȂāA͎͉̐̂A{炩Av̂̂̂‚ɂ]ƌ΂manɂ֎Q悤ɓɂłȂ̂ŁA\ɕςĂłōlBႦ΂Ƃǂ܂̂ۂނ݂ƂłāA̎ł͐\ĂƂĐԂɕׂ̂ɍsȂȁB


diff --git a/spec/fixtures/requests/sjis_with_wrong_charset.txt b/spec/fixtures/requests/sjis_with_wrong_charset.txt new file mode 100644 index 00000000000..456750c6b39 --- /dev/null +++ b/spec/fixtures/requests/sjis_with_wrong_charset.txt @@ -0,0 +1,20 @@ +HTTP/1.1 200 OK +Server: nginx/1.11.10 +Date: Tue, 04 Jul 2017 16:43:39 GMT +Content-Type: text/html; charset=utf-8 +Content-Length: 273 +Connection: keep-alive +Last-Modified: Tue, 04 Jul 2017 16:41:34 GMT +Accept-Ranges: bytes + + + + + + SJIS̃y[W + + +

N܂ĂLOlĂ̂̎łłBԂɈӖ҂͐ǂȔ܂܂ł\グ邽ɂ͎QlA邽Aɂ܂ȂB炢Ȃ̂͂ǂ㌎ł邾BĉcɔRKɉ]ł͂͂Ȃw}ƂoȂȂāA͎͉̐̂A{炩Av̂̂̂‚ɂ]ƌ΂manɂ֎Q悤ɓɂłȂ̂ŁA\ɕςĂłōlBႦ΂Ƃǂ܂̂ۂނ݂ƂłāA̎ł͐\ĂƂĐԂɕׂ̂ɍsȂȁB
+

+ + diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb index 7d7f8e74847..698eb032472 100644 --- a/spec/services/fetch_link_card_service_spec.rb +++ b/spec/services/fetch_link_card_service_spec.rb @@ -8,6 +8,10 @@ RSpec.describe FetchLinkCardService do stub_request(:get, 'http://example.xn--fiqs8s/').to_return(request_fixture('idn.txt')) stub_request(:head, 'http://example.com/sjis').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) stub_request(:get, 'http://example.com/sjis').to_return(request_fixture('sjis.txt')) + stub_request(:head, 'http://example.com/sjis_with_wrong_charset').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) + stub_request(:get, 'http://example.com/sjis_with_wrong_charset').to_return(request_fixture('sjis_with_wrong_charset.txt')) + stub_request(:head, 'http://example.com/koi8-r').to_return(status: 200, headers: { 'Content-Type' => 'text/html' }) + stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt')) stub_request(:head, 'https://github.com/qbi/WannaCry').to_return(status: 404) subject.call(status) @@ -27,6 +31,25 @@ RSpec.describe FetchLinkCardService do it 'works with SJIS' do expect(a_request(:get, 'http://example.com/sjis')).to have_been_made.at_least_once + expect(status.preview_card.title).to eq("SJISのページ") + end + end + + context do + let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis_with_wrong_charset') } + + it 'works with SJIS even with wrong charset header' do + expect(a_request(:get, 'http://example.com/sjis_with_wrong_charset')).to have_been_made.at_least_once + expect(status.preview_card.title).to eq("SJISのページ") + end + end + + context do + let(:status) { Fabricate(:status, text: 'Check out http://example.com/koi8-r') } + + it 'works with koi8-r' do + expect(a_request(:get, 'http://example.com/koi8-r')).to have_been_made.at_least_once + expect(status.preview_card.title).to eq("Московя начинаетъ только въ XVI ст. привлекать внимане иностранцевъ.") end end end From f68fa930ea448f5e94057160cfdcc78fec4aba11 Mon Sep 17 00:00:00 2001 From: Daniel Hunsaker Date: Sat, 8 Jul 2017 18:52:36 -0600 Subject: [PATCH 078/114] [nanobox] Allow Full-size Uploads (#4123) The Nginx configurations used by Nanobox previously neglected to increase the default upload size limit. This PR bumps that value up to the current Mastodon limit of 8MiB. --- nanobox/nginx-local.conf | 6 ++++-- nanobox/nginx-stream.conf.erb | 4 ++-- nanobox/nginx-web.conf.erb | 6 ++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/nanobox/nginx-local.conf b/nanobox/nginx-local.conf index 023328733b8..f56339cac9b 100644 --- a/nanobox/nginx-local.conf +++ b/nanobox/nginx-local.conf @@ -27,8 +27,8 @@ http { } map $http_upgrade $connection_upgrade { - default upgrade; - '' close; + default upgrade; + '' close; } # Configuration for Nginx @@ -38,6 +38,8 @@ http { root /app/public; + client_max_body_size 8M; + location / { try_files $uri @rails; } diff --git a/nanobox/nginx-stream.conf.erb b/nanobox/nginx-stream.conf.erb index b39d3ff1de5..2a047dd9f6d 100644 --- a/nanobox/nginx-stream.conf.erb +++ b/nanobox/nginx-stream.conf.erb @@ -22,8 +22,8 @@ http { } map $http_upgrade $connection_upgrade { - default upgrade; - '' close; + default upgrade; + '' close; } # Configuration for Nginx diff --git a/nanobox/nginx-web.conf.erb b/nanobox/nginx-web.conf.erb index 55245bf281a..24cd17cffda 100644 --- a/nanobox/nginx-web.conf.erb +++ b/nanobox/nginx-web.conf.erb @@ -22,8 +22,8 @@ http { } map $http_upgrade $connection_upgrade { - default upgrade; - '' close; + default upgrade; + '' close; } # Configuration for Nginx @@ -36,6 +36,8 @@ http { root /app/public; + client_max_body_size 8M; + location / { try_files $uri @rails; } From 37c832cdf7a307511b27e64174ed1a3e160ec66e Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Sun, 9 Jul 2017 12:16:08 +0200 Subject: [PATCH 079/114] refactor: Make all reducers sync (#4125) --- app/javascript/mastodon/actions/store.js | 7 ---- .../mastodon/containers/mastodon.js | 3 +- .../features/ui/util/async-components.js | 41 ++----------------- app/javascript/mastodon/reducers/compose.js | 4 +- app/javascript/mastodon/reducers/index.js | 15 +++---- .../mastodon/reducers/media_attachments.js | 4 +- .../mastodon/store/configureStore.js | 25 +---------- app/views/layouts/application.html.haml | 9 ---- 8 files changed, 18 insertions(+), 90 deletions(-) diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index 08c2810cae9..efdb0771a5a 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -16,10 +16,3 @@ export function hydrateStore(rawState) { state, }; }; - -export function hydrateStoreLazy(name, state) { - return { - type: `${STORE_HYDRATE_LAZY}-${name}`, - state, - }; -}; diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index 6e79f9e4f7f..87ab6023c49 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -23,8 +23,7 @@ const { localeData, messages } = getLocale(); addLocaleData(localeData); export const store = configureStore(); -const initialState = JSON.parse(document.getElementById('initial-state').textContent); -export const hydrateAction = hydrateStore(initialState); +const hydrateAction = hydrateStore(JSON.parse(document.getElementById('initial-state').textContent)); store.dispatch(hydrateAction); export default class Mastodon extends React.PureComponent { diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js index 56880dd1f65..55de114b517 100644 --- a/app/javascript/mastodon/features/ui/util/async-components.js +++ b/app/javascript/mastodon/features/ui/util/async-components.js @@ -1,40 +1,13 @@ -import { store } from '../../../containers/mastodon'; -import { refreshNotifications } from '../../../actions/notifications'; -import { injectAsyncReducer } from '../../../store/configureStore'; - -// NOTE: When lazy-loading reducers, make sure to add them -// to application.html.haml (if the component is preloaded there) - export function EmojiPicker () { return import(/* webpackChunkName: "emojione_picker" */'emojione-picker'); } export function Compose () { - return Promise.all([ - import(/* webpackChunkName: "features/compose" */'../../compose'), - import(/* webpackChunkName: "reducers/compose" */'../../../reducers/compose'), - import(/* webpackChunkName: "reducers/media_attachments" */'../../../reducers/media_attachments'), - import(/* webpackChunkName: "reducers/search" */'../../../reducers/search'), - ]).then(([component, composeReducer, mediaAttachmentsReducer, searchReducer]) => { - injectAsyncReducer(store, 'compose', composeReducer.default); - injectAsyncReducer(store, 'media_attachments', mediaAttachmentsReducer.default); - injectAsyncReducer(store, 'search', searchReducer.default); - - return component; - }); + return import(/* webpackChunkName: "features/compose" */'../../compose'); } export function Notifications () { - return Promise.all([ - import(/* webpackChunkName: "features/notifications" */'../../notifications'), - import(/* webpackChunkName: "reducers/notifications" */'../../../reducers/notifications'), - ]).then(([component, notificationsReducer]) => { - injectAsyncReducer(store, 'notifications', notificationsReducer.default); - - store.dispatch(refreshNotifications()); - - return component; - }); + return import(/* webpackChunkName: "features/notifications" */'../../notifications'); } export function HomeTimeline () { @@ -110,15 +83,7 @@ export function MediaModal () { } export function OnboardingModal () { - return Promise.all([ - import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal'), - import(/* webpackChunkName: "reducers/compose" */'../../../reducers/compose'), - import(/* webpackChunkName: "reducers/media_attachments" */'../../../reducers/media_attachments'), - ]).then(([component, composeReducer, mediaAttachmentsReducer]) => { - injectAsyncReducer(store, 'compose', composeReducer.default); - injectAsyncReducer(store, 'media_attachments', mediaAttachmentsReducer.default); - return component; - }); + return import(/* webpackChunkName: "modals/onboarding_modal" */'../components/onboarding_modal'); } export function VideoModal () { diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 09db95e2d7e..d0b47a85c2c 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -23,7 +23,7 @@ import { COMPOSE_EMOJI_INSERT, } from '../actions/compose'; import { TIMELINE_DELETE } from '../actions/timelines'; -import { STORE_HYDRATE_LAZY } from '../actions/store'; +import { STORE_HYDRATE } from '../actions/store'; import Immutable from 'immutable'; import uuid from '../uuid'; @@ -134,7 +134,7 @@ const privacyPreference = (a, b) => { export default function compose(state = initialState, action) { switch(action.type) { - case `${STORE_HYDRATE_LAZY}-compose`: + case STORE_HYDRATE: return clearAll(state.merge(action.state.get('compose'))); case COMPOSE_MOUNT: return state.set('mounted', true); diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 79062f2f9c2..919345f165b 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -14,6 +14,10 @@ import status_lists from './status_lists'; import cards from './cards'; import reports from './reports'; import contexts from './contexts'; +import compose from './compose'; +import search from './search'; +import media_attachments from './media_attachments'; +import notifications from './notifications'; const reducers = { timelines, @@ -31,13 +35,10 @@ const reducers = { cards, reports, contexts, + compose, + search, + media_attachments, + notifications, }; -export function createReducer(asyncReducers) { - return combineReducers({ - ...reducers, - ...asyncReducers, - }); -} - export default combineReducers(reducers); diff --git a/app/javascript/mastodon/reducers/media_attachments.js b/app/javascript/mastodon/reducers/media_attachments.js index d17d465aa49..85bea4f0b38 100644 --- a/app/javascript/mastodon/reducers/media_attachments.js +++ b/app/javascript/mastodon/reducers/media_attachments.js @@ -1,4 +1,4 @@ -import { STORE_HYDRATE_LAZY } from '../actions/store'; +import { STORE_HYDRATE } from '../actions/store'; import Immutable from 'immutable'; const initialState = Immutable.Map({ @@ -7,7 +7,7 @@ const initialState = Immutable.Map({ export default function meta(state = initialState, action) { switch(action.type) { - case `${STORE_HYDRATE_LAZY}-media_attachments`: + case STORE_HYDRATE: return state.merge(action.state.get('media_attachments')); default: return state; diff --git a/app/javascript/mastodon/store/configureStore.js b/app/javascript/mastodon/store/configureStore.js index 0fe29f031a2..1376d4cbaf5 100644 --- a/app/javascript/mastodon/store/configureStore.js +++ b/app/javascript/mastodon/store/configureStore.js @@ -1,36 +1,15 @@ import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; -import appReducer, { createReducer } from '../reducers'; -import { hydrateStoreLazy } from '../actions/store'; -import { hydrateAction } from '../containers/mastodon'; +import appReducer from '../reducers'; import loadingBarMiddleware from '../middleware/loading_bar'; import errorsMiddleware from '../middleware/errors'; import soundsMiddleware from '../middleware/sounds'; export default function configureStore() { - const store = createStore(appReducer, compose(applyMiddleware( + return createStore(appReducer, compose(applyMiddleware( thunk, loadingBarMiddleware({ promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAIL'] }), errorsMiddleware(), soundsMiddleware() ), window.devToolsExtension ? window.devToolsExtension() : f => f)); - - store.asyncReducers = { }; - - return store; }; - -export function injectAsyncReducer(store, name, asyncReducer) { - if (!store.asyncReducers[name]) { - // Keep track that we injected this reducer - store.asyncReducers[name] = asyncReducer; - - // Add the current reducer to the store - store.replaceReducer(createReducer(store.asyncReducers)); - - // The state this reducer handles defaults to its initial state (stored inside the reducer) - // But that state may be out of date because of the server-side hydration, so we replay - // the hydration action but only for this reducer (all async reducers must listen for this dynamic action) - store.dispatch(hydrateStoreLazy(name, hydrateAction.state)); - } -} diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 68d3468590d..580d8fb4d29 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -22,19 +22,10 @@ = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' = javascript_pack_tag 'features/getting_started', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'features/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'reducers/compose', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'reducers/media_attachments', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'reducers/search', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'features/home_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'features/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'reducers/notifications', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'features/community_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' - = javascript_pack_tag 'features/public_timeline', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' From 8781a8e2030b6b75d9412cddd7a696506c4633f0 Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Sun, 9 Jul 2017 12:17:00 +0200 Subject: [PATCH 080/114] i18n: minor change (pl) (#4124) --- config/locales/pl.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/pl.yml b/config/locales/pl.yml index bf9d5e034b6..0dc1da8b44e 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -392,7 +392,7 @@ pl: unlisted_long: Widoczne dla wszystkich, ale nie wyświetlane na publicznych osiach czasu stream_entries: click_to_show: Naciśnij aby wyświetlić - reblogged: podbito + reblogged: podbił sensitive_content: Wrażliwa zawartość terms: body_html: | From ce3a371eeeaae35e49f1938ede5eb6105c764fbc Mon Sep 17 00:00:00 2001 From: unarist Date: Sun, 9 Jul 2017 20:04:30 +0900 Subject: [PATCH 081/114] Fix initial loading of pinned Notifications column (#4126) --- app/javascript/mastodon/features/ui/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 8d784dfe02e..3baf09b9325 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -11,6 +11,7 @@ import { isMobile } from '../../is_mobile'; import { debounce } from 'lodash'; import { uploadCompose } from '../../actions/compose'; import { refreshHomeTimeline } from '../../actions/timelines'; +import { refreshNotifications } from '../../actions/notifications'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; import ColumnsAreaContainer from './containers/columns_area_container'; @@ -129,6 +130,7 @@ export default class UI extends React.PureComponent { document.addEventListener('dragend', this.handleDragEnd, false); this.props.dispatch(refreshHomeTimeline()); + this.props.dispatch(refreshNotifications()); } componentWillUnmount () { From caf938562ef0d0fdb03bf57f15bbab8d76c5b4c0 Mon Sep 17 00:00:00 2001 From: unarist Date: Sun, 9 Jul 2017 21:52:03 +0900 Subject: [PATCH 082/114] Avoid async import if the component is previously loaded (#4127) --- .../mastodon/features/ui/components/bundle.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/javascript/mastodon/features/ui/components/bundle.js b/app/javascript/mastodon/features/ui/components/bundle.js index 3eed446fec2..72798f69061 100644 --- a/app/javascript/mastodon/features/ui/components/bundle.js +++ b/app/javascript/mastodon/features/ui/components/bundle.js @@ -26,6 +26,8 @@ class Bundle extends React.Component { onFetchFail: noop, } + static cache = {} + state = { mod: undefined, forceRender: false, @@ -58,8 +60,17 @@ class Bundle extends React.Component { 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(); }) From fc4c74660b690038ae48264f9d5b0230df58acc4 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Sun, 9 Jul 2017 15:02:26 +0200 Subject: [PATCH 083/114] Swipeable views (#4105) * feat: Replace react-swipeable with react-swipeable-views * fix: iOS 9 --- .../mastodon/components/column_header.js | 2 +- .../features/ui/components/column_loading.js | 10 +++- .../features/ui/components/columns_area.js | 48 +++++++++++-------- .../features/ui/components/media_modal.js | 18 ++++--- .../ui/components/onboarding_modal.js | 40 ++++++---------- .../features/ui/components/tabs_bar.js | 26 +++------- app/javascript/styles/components.scss | 40 +++++++++++++++- package.json | 2 +- yarn.lock | 46 +++++++++++++++--- 9 files changed, 150 insertions(+), 82 deletions(-) diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index ec9379320fa..5b2a4d84c4c 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -10,7 +10,7 @@ export default class ColumnHeader extends React.PureComponent { }; static propTypes = { - title: PropTypes.string.isRequired, + title: PropTypes.node.isRequired, icon: PropTypes.string.isRequired, active: PropTypes.bool, multiColumn: PropTypes.bool, diff --git a/app/javascript/mastodon/features/ui/components/column_loading.js b/app/javascript/mastodon/features/ui/components/column_loading.js index 9bb9c14a103..7ecfaf77a51 100644 --- a/app/javascript/mastodon/features/ui/components/column_loading.js +++ b/app/javascript/mastodon/features/ui/components/column_loading.js @@ -1,13 +1,19 @@ import React from 'react'; +import PropTypes from 'prop-types'; import Column from '../../../components/column'; import ColumnHeader from '../../../components/column_header'; -const ColumnLoading = () => ( +const ColumnLoading = ({ title = '', icon = ' ' }) => ( - +
); +ColumnLoading.propTypes = { + title: PropTypes.node, + icon: PropTypes.string, +}; + export default ColumnLoading; diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 5fa27599fba..9ff91377497 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -3,8 +3,8 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import ReactSwipeable from 'react-swipeable'; -import { getPreviousLink, getNextLink } from './tabs_bar'; +import ReactSwipeableViews from 'react-swipeable-views'; +import { links, getIndex, getLink } from './tabs_bar'; import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; @@ -32,21 +32,29 @@ export default class ColumnsArea extends ImmutablePureComponent { children: PropTypes.node, }; - handleRightSwipe = () => { - const previousLink = getPreviousLink(this.context.router.history.location.pathname); - - if (previousLink) { - this.context.router.history.push(previousLink); - } + handleSwipe = (index) => { + window.requestAnimationFrame(() => { + window.requestAnimationFrame(() => { + this.context.router.history.push(getLink(index)); + }); + }); } - handleLeftSwipe = () => { - const previousLink = getNextLink(this.context.router.history.location.pathname); + renderView = (link, index) => { + 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) { - this.context.router.history.push(previousLink); - } - }; + const view = (index === columnIndex) ? + React.cloneElement(this.props.children) : + ; + + return ( +
+ {view} +
+ ); + } renderLoading = () => { return ; @@ -59,12 +67,14 @@ export default class ColumnsArea extends ImmutablePureComponent { render () { const { columns, children, singleColumn } = this.props; + const columnIndex = getIndex(this.context.router.history.location.pathname); + if (singleColumn) { - return ( - - {children} - - ); + return columnIndex !== -1 ? ( + + {links.map(this.renderView)} + + ) :
{children}>
; } return ( diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index a5b9dc19f30..769e1882028 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -1,5 +1,5 @@ import React from 'react'; -import ReactSwipeable from 'react-swipeable'; +import ReactSwipeableViews from 'react-swipeable-views'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import ExtendedVideoPlayer from '../../../components/extended_video_player'; @@ -26,6 +26,10 @@ export default class MediaModal extends ImmutablePureComponent { index: null, }; + handleSwipe = (index) => { + this.setState({ index: (index) % this.props.media.size }); + } + handleNextClick = () => { this.setState({ index: (this.getIndex() + 1) % this.props.media.size }); } @@ -74,10 +78,12 @@ export default class MediaModal extends ImmutablePureComponent { } if (attachment.get('type') === 'image') { - const width = attachment.getIn(['meta', 'original', 'width']) || null; - const height = attachment.getIn(['meta', 'original', 'height']) || null; + content = media.map((image) => { + const width = image.getIn(['meta', 'original', 'width']) || null; + const height = image.getIn(['meta', 'original', 'height']) || null; - content = ; + return ; + }).toArray(); } else if (attachment.get('type') === 'gifv') { content = ; } @@ -88,9 +94,9 @@ export default class MediaModal extends ImmutablePureComponent {
- + {content} - +
{rightNav} diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js index b056357a226..189bd86651b 100644 --- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js +++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js @@ -3,11 +3,9 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import ReactSwipeable from 'react-swipeable'; +import ReactSwipeableViews from 'react-swipeable-views'; import classNames from 'classnames'; 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 Search from '../../compose/components/search'; import NavigationBar from '../../compose/components/navigation_bar'; @@ -227,6 +225,10 @@ export default class OnboardingModal extends React.PureComponent { })); } + handleSwipe = (index) => { + this.setState({ currentIndex: index }); + } + handleKeyUp = ({ key }) => { switch (key) { case 'ArrowLeft': @@ -263,30 +265,18 @@ export default class OnboardingModal extends React.PureComponent { ); - const styles = pages.map((data, i) => ({ - key: `page-${i}`, - data, - style: { - opacity: spring(i === currentIndex ? 1 : 0), - }, - })); - return (
- - {interpolatedStyles => ( - - {interpolatedStyles.map(({ key, data, style }, i) => { - const className = classNames('onboarding-modal__page__wrapper', { - 'onboarding-modal__page__wrapper--active': i === currentIndex, - }); - return ( -
{data}
- ); - })} -
- )} -
+ + {pages.map((page, i) => { + const className = classNames('onboarding-modal__page__wrapper', { + 'onboarding-modal__page__wrapper--active': i === currentIndex, + }); + return ( +
{page}
+ ); + })} +
diff --git a/app/javascript/mastodon/features/ui/components/tabs_bar.js b/app/javascript/mastodon/features/ui/components/tabs_bar.js index c2e6c88b516..b4153ff459c 100644 --- a/app/javascript/mastodon/features/ui/components/tabs_bar.js +++ b/app/javascript/mastodon/features/ui/components/tabs_bar.js @@ -2,7 +2,7 @@ import React from 'react'; import NavLink from 'react-router-dom/NavLink'; import { FormattedMessage } from 'react-intl'; -const links = [ +export const links = [ , , , @@ -13,25 +13,13 @@ const links = [ , ]; -export function getPreviousLink (path) { - const index = links.findIndex(link => link.props.to === path); +export function getIndex (path) { + return links.findIndex(link => link.props.to === path); +} - if (index > 0) { - return links[index - 1].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 function getLink (index) { + return links[index].props.to; +} export default class TabsBar extends React.Component { diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 66d2715da07..397126ec1db 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1266,6 +1266,23 @@ .columns-area { padding: 10px; } + + .react-swipeable-view-container .columns-area { + height: calc(100% - 20px) !important; + } +} + +.react-swipeable-view-container { + &, + .columns-area, + .drawer, + .column { + height: 100%; + } +} + +.react-swipeable-view-container > * { + height: 100%; } .column { @@ -2910,7 +2927,7 @@ button.icon-button.active i.fa-retweet { video { max-width: 80vw; max-height: 80vh; - width: auto; + width: 100%; height: auto; } @@ -2938,7 +2955,26 @@ button.icon-button.active i.fa-retweet { flex-direction: column; } -.onboarding-modal__pager, +.onboarding-modal__pager { + height: 80vh; + width: 80vw; + max-width: 520px; + max-height: 420px; + + .react-swipeable-view-container > div { + width: 100%; + height: 100%; + box-sizing: border-box; + padding: 25px; + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + display: flex; + user-select: text; + } +} + .error-modal__body { height: 80vh; width: 80vw; diff --git a/package.json b/package.json index 2f63d0bbdfc..94545c47e28 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "react-router-dom": "^4.1.1", "react-router-scroll": "ytase/react-router-scroll#build", "react-simple-dropdown": "^3.0.0", - "react-swipeable": "^4.0.1", + "react-swipeable-views": "^0.12.3", "react-textarea-autosize": "^5.0.7", "react-toggle": "^4.0.1", "redis": "^2.7.1", diff --git a/yarn.lock b/yarn.lock index d8ed20343d5..d4295cb502c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1259,7 +1259,7 @@ babel-register@^6.24.1: mkdirp "^0.5.1" source-map-support "^0.4.2" -babel-runtime@6.x.x, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.5.0, babel-runtime@^6.9.2: +babel-runtime@6.x.x, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.2.0, babel-runtime@^6.20.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.5.0, babel-runtime@^6.9.2: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b" dependencies: @@ -2318,7 +2318,7 @@ doctrine@^2.0.0: esutils "^2.0.2" isarray "^1.0.0" -"dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.0.0: +"dom-helpers@^2.4.0 || ^3.0.0", dom-helpers@^3.0.0, dom-helpers@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.2.1.tgz#3203e07fed217bd1f424b019735582fc37b2825a" @@ -3937,7 +3937,7 @@ jsx-ast-utils@^1.0.0, jsx-ast-utils@^1.3.4: version "1.4.1" resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz#3867213e8dd79bf1e8f2300c0cfc1efb182c0df1" -keycode@^2.1.8: +keycode@^2.1.7, keycode@^2.1.8: version "2.1.9" resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.1.9.tgz#964a23c54e4889405b4861a5c9f0480d45141dfa" @@ -5711,6 +5711,15 @@ react-element-to-jsx-string@^5.0.0: stringify-object "2.4.0" traverse "^0.6.6" +react-event-listener@^0.4.5: + version "0.4.5" + resolved "https://registry.yarnpkg.com/react-event-listener/-/react-event-listener-0.4.5.tgz#e3e895a0970cf14ee8f890113af68197abf3d0b1" + dependencies: + babel-runtime "^6.20.0" + fbjs "^0.8.4" + prop-types "^15.5.4" + warning "^3.0.0" + react-html-attributes@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/react-html-attributes/-/react-html-attributes-1.3.0.tgz#c97896e9cac47ad9c4e6618b835029a826f5d28c" @@ -5875,11 +5884,34 @@ react-style-proptype@^3.0.0: dependencies: prop-types "^15.5.4" -react-swipeable@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/react-swipeable/-/react-swipeable-4.0.1.tgz#2cb3a04a52ccebf5361881b30e233dc13ee4b115" +react-swipeable-views-core@^0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/react-swipeable-views-core/-/react-swipeable-views-core-0.11.1.tgz#61d046799f90725bbf91a0eb3abcab805c774cac" dependencies: - prop-types "^15.5.8" + babel-runtime "^6.23.0" + warning "^3.0.0" + +react-swipeable-views-utils@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/react-swipeable-views-utils/-/react-swipeable-views-utils-0.12.0.tgz#4ff11f20a8da0561f623876d9fd691116e1a6a03" + dependencies: + babel-runtime "^6.23.0" + fbjs "^0.8.4" + keycode "^2.1.7" + prop-types "^15.5.4" + react-event-listener "^0.4.5" + react-swipeable-views-core "^0.11.1" + +react-swipeable-views@^0.12.3: + version "0.12.3" + resolved "https://registry.yarnpkg.com/react-swipeable-views/-/react-swipeable-views-0.12.3.tgz#b0d3f417bcbcd06afda2f8437c15e8360a568744" + dependencies: + babel-runtime "^6.23.0" + dom-helpers "^3.2.1" + prop-types "^15.5.4" + react-swipeable-views-core "^0.11.1" + react-swipeable-views-utils "^0.12.0" + warning "^3.0.0" react-test-renderer@^15.6.1: version "15.6.1" From 307f3e0dd77b338669648f830f7f74103d2d226f Mon Sep 17 00:00:00 2001 From: abcang Date: Mon, 10 Jul 2017 00:33:21 +0900 Subject: [PATCH 084/114] Rescue exceptions related to Goldfinger (#4044) * Rescue exceptions related to Goldfinger * Exclude Goldfinger::SSLError --- app/services/fetch_remote_account_service.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/services/fetch_remote_account_service.rb b/app/services/fetch_remote_account_service.rb index 8eed0d45429..1efac365b0c 100644 --- a/app/services/fetch_remote_account_service.rb +++ b/app/services/fetch_remote_account_service.rb @@ -32,5 +32,8 @@ class FetchRemoteAccountService < BaseService rescue Nokogiri::XML::XPath::SyntaxError Rails.logger.debug 'Invalid XML or missing namespace' nil + rescue Goldfinger::NotFoundError, Goldfinger::Error + Rails.logger.debug 'Exceptions related to Goldfinger occurs' + nil end end From 5fa2dd6e652ac6400b793602c446fcd55000d498 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Sun, 9 Jul 2017 08:34:05 -0700 Subject: [PATCH 085/114] Use babel-plugin-transform-react-inline-elements (#4109) --- .babelrc | 1 + package.json | 1 + yarn.lock | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/.babelrc b/.babelrc index 292d52e2775..19968964eee 100644 --- a/.babelrc +++ b/.babelrc @@ -44,6 +44,7 @@ ] } ], + "transform-react-inline-elements", [ "transform-runtime", { diff --git a/package.json b/package.json index 94545c47e28..4c5a3f1d9dc 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-es2015-modules-commonjs": "^6.24.1", "babel-plugin-transform-object-rest-spread": "^6.23.0", + "babel-plugin-transform-react-inline-elements": "^6.22.0", "babel-plugin-transform-react-jsx-self": "^6.22.0", "babel-plugin-transform-react-jsx-source": "^6.22.0", "babel-plugin-transform-react-remove-prop-types": "^0.4.6", diff --git a/yarn.lock b/yarn.lock index d4295cb502c..aedbde6be61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1027,6 +1027,12 @@ babel-plugin-transform-react-display-name@^6.23.0: dependencies: babel-runtime "^6.22.0" +babel-plugin-transform-react-inline-elements@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-inline-elements/-/babel-plugin-transform-react-inline-elements-6.22.0.tgz#6687211a32b49a52f22c573a2b5504a25ef17c53" + dependencies: + babel-runtime "^6.22.0" + babel-plugin-transform-react-jsx-self@6.22.0, babel-plugin-transform-react-jsx-self@^6.22.0: version "6.22.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-jsx-self/-/babel-plugin-transform-react-jsx-self-6.22.0.tgz#df6d80a9da2612a121e6ddd7558bcbecf06e636e" From 4122a837fadf8cf59712b5c1790ac0af96bcbc84 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Sun, 9 Jul 2017 18:49:07 +0200 Subject: [PATCH 086/114] fix(components/media_modal): Aspect ratio (#4128) * fix(components/media_modal): Aspect ratio * fix: Remove useless style --- app/javascript/mastodon/features/ui/components/image_loader.js | 1 + app/javascript/mastodon/features/ui/components/media_modal.js | 2 +- app/javascript/styles/components.scss | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/ui/components/image_loader.js b/app/javascript/mastodon/features/ui/components/image_loader.js index 5ea55d1d26f..aad594380e8 100644 --- a/app/javascript/mastodon/features/ui/components/image_loader.js +++ b/app/javascript/mastodon/features/ui/components/image_loader.js @@ -133,6 +133,7 @@ export default class ImageLoader extends React.PureComponent { width={width} height={height} ref={this.setCanvasRef} + style={{ opacity: loading ? 1 : 0 }} /> {!loading && ( diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index 769e1882028..23f588669f1 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -94,7 +94,7 @@ export default class MediaModal extends ImmutablePureComponent {
- + {content}
diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 397126ec1db..97651b5f46d 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -2935,6 +2935,7 @@ button.icon-button.active i.fa-retweet { canvas { display: block; background: url('../images/void.png') repeat; + object-fit: contain; } } From f8212da329cf857478746c23314e5c662cd490e3 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 10 Jul 2017 10:29:34 +0900 Subject: [PATCH 087/114] Add attribute for default privacy to verify credentials (#4075) * Add attribute for default privacy to verify credentials * add raw_note * source --- .../api/v1/accounts/credentials_controller.rb | 4 ++-- .../rest/credential_account_serializer.rb | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 app/serializers/rest/credential_account_serializer.rb diff --git a/app/controllers/api/v1/accounts/credentials_controller.rb b/app/controllers/api/v1/accounts/credentials_controller.rb index 8ee9a2416e9..073808532a5 100644 --- a/app/controllers/api/v1/accounts/credentials_controller.rb +++ b/app/controllers/api/v1/accounts/credentials_controller.rb @@ -6,13 +6,13 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController def show @account = current_account - render json: @account, serializer: REST::AccountSerializer + render json: @account, serializer: REST::CredentialAccountSerializer end def update current_account.update!(account_params) @account = current_account - render json: @account, serializer: REST::AccountSerializer + render json: @account, serializer: REST::CredentialAccountSerializer end private diff --git a/app/serializers/rest/credential_account_serializer.rb b/app/serializers/rest/credential_account_serializer.rb new file mode 100644 index 00000000000..094b831c964 --- /dev/null +++ b/app/serializers/rest/credential_account_serializer.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class REST::CredentialAccountSerializer < REST::AccountSerializer + attributes :source + + def source + user = object.user + { + privacy: user.setting_default_privacy, + note: object.note, + } + end +end From 1c6cbdd4e4894cc5c8928fe1d6f45d781049a59c Mon Sep 17 00:00:00 2001 From: Lynx Kotoura Date: Mon, 10 Jul 2017 11:37:10 +0900 Subject: [PATCH 088/114] Fix duplication of tag in columns_area.js (#4131) Deleted ">" just a typo. --- app/javascript/mastodon/features/ui/components/columns_area.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 9ff91377497..cbc185a7d60 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -74,7 +74,7 @@ export default class ColumnsArea extends ImmutablePureComponent { {links.map(this.renderView)} - ) :
{children}>
; + ) :
{children}
; } return ( From 4aa6cd66fce2940b1a26859de942fc5ec7ea4127 Mon Sep 17 00:00:00 2001 From: Sadiq Saif Date: Sun, 9 Jul 2017 22:49:48 -0400 Subject: [PATCH 089/114] Change default for auto_play_fit to false for a11y (#4132) This is per issue #3876 --- config/settings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings.yml b/config/settings.yml index 18b70b51f06..be2a7a1f834 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -17,7 +17,7 @@ defaults: &defaults closed_registrations_message: '' open_deletion: true boost_modal: false - auto_play_gif: true + auto_play_gif: false delete_modal: true system_font_ui: false notification_emails: From 617208053c2bf935d2dd3944bb2b9192a388f0b4 Mon Sep 17 00:00:00 2001 From: abcang Date: Mon, 10 Jul 2017 20:59:29 +0900 Subject: [PATCH 090/114] Rescue exceptions related to Goldfinger at FetchRemoteStatusService (#4138) --- app/services/fetch_remote_status_service.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/services/fetch_remote_status_service.rb b/app/services/fetch_remote_status_service.rb index f414813ada5..4cfd33d90a0 100644 --- a/app/services/fetch_remote_status_service.rb +++ b/app/services/fetch_remote_status_service.rb @@ -33,6 +33,9 @@ class FetchRemoteStatusService < BaseService rescue Nokogiri::XML::XPath::SyntaxError Rails.logger.debug 'Invalid XML or missing namespace' nil + rescue Goldfinger::NotFoundError, Goldfinger::Error + Rails.logger.debug 'Exceptions related to Goldfinger occurs' + nil end def confirmed_domain?(domain, account) From 2b9721d1b38319d70bed98e76a0fe1d648780298 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 10 Jul 2017 21:00:32 +0900 Subject: [PATCH 091/114] Add setting a always mark media as sensitive (#4136) --- app/controllers/settings/preferences_controller.rb | 1 + app/javascript/mastodon/reducers/compose.js | 7 +++++++ app/lib/user_settings_decorator.rb | 5 +++++ app/models/user.rb | 4 ++++ app/serializers/initial_state_serializer.rb | 1 + app/serializers/rest/credential_account_serializer.rb | 1 + app/views/settings/preferences/show.html.haml | 2 ++ config/locales/simple_form.en.yml | 1 + spec/lib/user_settings_decorator_spec.rb | 7 +++++++ 9 files changed, 29 insertions(+) diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index a15c26031bf..cac5b0ba8f6 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -34,6 +34,7 @@ class Settings::PreferencesController < ApplicationController def user_settings_params params.require(:user).permit( :setting_default_privacy, + :setting_default_sensitive, :setting_boost_modal, :setting_delete_modal, :setting_auto_play_gif, diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index d0b47a85c2c..7523777394f 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -45,6 +45,7 @@ const initialState = Immutable.Map({ suggestions: Immutable.List(), me: null, default_privacy: 'public', + default_sensitive: false, resetFileKey: Math.floor((Math.random() * 0x10000)), idempotencyKey: null, }); @@ -75,6 +76,8 @@ function clearAll(state) { }; function appendMedia(state, media) { + const prevSize = state.get('media_attachments').size; + return state.withMutations(map => { map.update('media_attachments', list => list.push(media)); map.set('is_uploading', false); @@ -82,6 +85,10 @@ function appendMedia(state, media) { map.update('text', oldText => `${oldText.trim()} ${media.get('text_url')}`); map.set('focusDate', new Date()); map.set('idempotencyKey', uuid()); + + if (prevSize === 0 && state.get('default_sensitive')) { + map.set('sensitive', true); + } }); }; diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 9c0cb454597..e0e92b19d06 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -18,6 +18,7 @@ class UserSettingsDecorator user.settings['notification_emails'] = merged_notification_emails user.settings['interactions'] = merged_interactions user.settings['default_privacy'] = default_privacy_preference + user.settings['default_sensitive'] = default_sensitive_preference user.settings['boost_modal'] = boost_modal_preference user.settings['delete_modal'] = delete_modal_preference user.settings['auto_play_gif'] = auto_play_gif_preference @@ -36,6 +37,10 @@ class UserSettingsDecorator settings['setting_default_privacy'] end + def default_sensitive_preference + boolean_cast_setting 'setting_default_sensitive' + end + def boost_modal_preference boolean_cast_setting 'setting_boost_modal' end diff --git a/app/models/user.rb b/app/models/user.rb index e2bb3d0ed20..c80115a08e3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -79,6 +79,10 @@ class User < ApplicationRecord settings.default_privacy || (account.locked? ? 'private' : 'public') end + def setting_default_sensitive + settings.default_sensitive + end + def setting_boost_modal settings.boost_modal end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 84f9e23a663..49ff9e37788 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -23,6 +23,7 @@ class InitialStateSerializer < ActiveModel::Serializer { me: object.current_account.id, default_privacy: object.current_account.user.setting_default_privacy, + default_sensitive: object.current_account.user.setting_default_sensitive, } end diff --git a/app/serializers/rest/credential_account_serializer.rb b/app/serializers/rest/credential_account_serializer.rb index 094b831c964..870d8b71f02 100644 --- a/app/serializers/rest/credential_account_serializer.rb +++ b/app/serializers/rest/credential_account_serializer.rb @@ -7,6 +7,7 @@ class REST::CredentialAccountSerializer < REST::AccountSerializer user = object.user { privacy: user.setting_default_privacy, + sensitive: user.setting_default_sensitive, note: object.note, } end diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 26fbfdf82ab..56a261ab65d 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -24,6 +24,8 @@ = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + = f.input :setting_default_sensitive, as: :boolean, wrapper: :with_label + .fields-group = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| = ff.input :follow, as: :boolean, wrapper: :with_label diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index d8d3b8a6fb7..fc5ab5ec8ca 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -37,6 +37,7 @@ en: setting_auto_play_gif: Auto-play animated GIFs setting_boost_modal: Show confirmation dialog before boosting setting_default_privacy: Post privacy + setting_default_sensitive: Always mark media as sensitive setting_delete_modal: Show confirmation dialog before deleting a toot setting_system_font_ui: Use system's default font severity: Severity diff --git a/spec/lib/user_settings_decorator_spec.rb b/spec/lib/user_settings_decorator_spec.rb index e1ba56d9754..a67487779fc 100644 --- a/spec/lib/user_settings_decorator_spec.rb +++ b/spec/lib/user_settings_decorator_spec.rb @@ -28,6 +28,13 @@ describe UserSettingsDecorator do expect(user.settings['default_privacy']).to eq 'public' end + it 'updates the user settings value for sensitive' do + values = { 'setting_default_sensitive' => '1' } + + settings.update(values) + expect(user.settings['default_sensitive']).to eq true + end + it 'updates the user settings value for boost modal' do values = { 'setting_boost_modal' => '1' } From 63baab088d734a982d21a5df538db554091188ad Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 10 Jul 2017 21:02:18 +0900 Subject: [PATCH 092/114] Fix regular expression for RFC 5646 (regression from #3604) (#4133) --- app/javascript/mastodon/locales/ar.json | 7 ++- app/javascript/mastodon/locales/bg.json | 7 ++- app/javascript/mastodon/locales/ca.json | 7 ++- app/javascript/mastodon/locales/de.json | 7 ++- .../mastodon/locales/defaultMessages.json | 55 ++++++++++++------- app/javascript/mastodon/locales/en.json | 7 ++- app/javascript/mastodon/locales/eo.json | 7 ++- app/javascript/mastodon/locales/es.json | 7 ++- app/javascript/mastodon/locales/fa.json | 7 ++- app/javascript/mastodon/locales/fi.json | 7 ++- app/javascript/mastodon/locales/fr.json | 7 ++- app/javascript/mastodon/locales/he.json | 7 ++- app/javascript/mastodon/locales/hr.json | 7 ++- app/javascript/mastodon/locales/hu.json | 7 ++- app/javascript/mastodon/locales/id.json | 7 ++- app/javascript/mastodon/locales/io.json | 7 ++- app/javascript/mastodon/locales/it.json | 7 ++- app/javascript/mastodon/locales/ja.json | 7 ++- app/javascript/mastodon/locales/ko.json | 7 ++- app/javascript/mastodon/locales/nl.json | 7 ++- app/javascript/mastodon/locales/no.json | 7 ++- app/javascript/mastodon/locales/oc.json | 7 ++- app/javascript/mastodon/locales/pl.json | 7 ++- app/javascript/mastodon/locales/pt-BR.json | 9 ++- app/javascript/mastodon/locales/pt.json | 7 ++- app/javascript/mastodon/locales/ru.json | 7 ++- app/javascript/mastodon/locales/th.json | 7 ++- app/javascript/mastodon/locales/tr.json | 7 ++- app/javascript/mastodon/locales/uk.json | 7 ++- .../mastodon/locales/whitelist_zh-HK.json | 1 - app/javascript/mastodon/locales/zh-CN.json | 9 ++- app/javascript/mastodon/locales/zh-HK.json | 9 ++- app/javascript/mastodon/locales/zh-TW.json | 9 ++- config/webpack/translationRunner.js | 2 +- 34 files changed, 221 insertions(+), 62 deletions(-) diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json index c13bc73d326..6992e7e0f7e 100644 --- a/app/javascript/mastodon/locales/ar.json +++ b/app/javascript/mastodon/locales/ar.json @@ -18,6 +18,12 @@ "account.unfollow": "إلغاء المتابعة", "account.unmute": "إلغاء الكتم عن @{name}", "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.community": "الخيط العام المحلي", "column.favourites": "المفضلة", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "لا تقم بإدراجه على الخيوط العامة", "privacy.unlisted.short": "غير مدرج", "reply_indicator.cancel": "إلغاء", - "report.heading": "تقرير جديد", "report.placeholder": "تعليقات إضافية", "report.submit": "إرسال", "report.target": "إبلاغ", diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 3b6f228c694..7a56e1446e1 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -18,6 +18,12 @@ "account.unfollow": "Не следвай", "account.unmute": "Unmute @{name}", "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.community": "Local timeline", "column.favourites": "Favourites", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Отказ", - "report.heading": "New report", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 8e8c95d5649..b2673915a38 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -18,6 +18,12 @@ "account.unfollow": "Deixar de seguir", "account.unmute": "Treure silenci de @{name}", "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.community": "Línia de temps local", "column.favourites": "Favorits", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "No publicar en línies de temps públiques", "privacy.unlisted.short": "No llistat", "reply_indicator.cancel": "Cancel·lar", - "report.heading": "Nou informe", "report.placeholder": "Comentaris addicionals", "report.submit": "Enviar", "report.target": "Informes", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 55499c0a352..4b62403c3a1 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -18,6 +18,12 @@ "account.unfollow": "Entfolgen", "account.unmute": "@{name} nicht mehr stummschalten", "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.community": "Lokale Zeitleiste", "column.favourites": "Favoriten", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Nicht in öffentlichen Zeitleisten anzeigen", "privacy.unlisted.short": "Nicht gelistet", "reply_indicator.cancel": "Abbrechen", - "report.heading": "Neue Meldung", "report.placeholder": "Zusätzliche Kommentare", "report.submit": "Absenden", "report.target": "Melden", diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index ccf2e6303c6..88f0f9c30a6 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -928,27 +928,6 @@ ], "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": [ { @@ -1008,6 +987,40 @@ ], "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": [ { diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 253db711054..778f33269f6 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -18,6 +18,12 @@ "account.unfollow": "Unfollow", "account.unmute": "Unmute @{name}", "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.community": "Local timeline", "column.favourites": "Favourites", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Do not post to public timelines", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Cancel", - "report.heading": "Report {target}", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting {target}", diff --git a/app/javascript/mastodon/locales/eo.json b/app/javascript/mastodon/locales/eo.json index 330fe831ded..2648a68401c 100644 --- a/app/javascript/mastodon/locales/eo.json +++ b/app/javascript/mastodon/locales/eo.json @@ -18,6 +18,12 @@ "account.unfollow": "Malsekvi", "account.unmute": "Unmute @{name}", "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.community": "Loka tempolinio", "column.favourites": "Favourites", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Rezigni", - "report.heading": "New report", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 6469aa6f2c6..c42930380f1 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -18,6 +18,12 @@ "account.unfollow": "Dejar de seguir", "account.unmute": "Unmute @{name}", "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.community": "Historia local", "column.favourites": "Favoritos", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "No mostrar en la historia federada", "privacy.unlisted.short": "Sin federar", "reply_indicator.cancel": "Cancelar", - "report.heading": "New report", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index 3835caab1db..c9f1888b54c 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -18,6 +18,12 @@ "account.unfollow": "پایان پیگیری", "account.unmute": "باصدا کردن @{name}", "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.community": "نوشته‌های محلی", "column.favourites": "پسندیده‌ها", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "عمومی، ولی فهرست نکن", "privacy.unlisted.short": "فهرست‌نشده", "reply_indicator.cancel": "لغو", - "report.heading": "گزارش تازه", "report.placeholder": "توضیح اضافه", "report.submit": "بفرست", "report.target": "گزارش‌دادن", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index dae9117996d..b836d2f5df6 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -18,6 +18,12 @@ "account.unfollow": "Lopeta seuraaminen", "account.unmute": "Unmute @{name}", "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.community": "Paikallinen aikajana", "column.favourites": "Favourites", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Peruuta", - "report.heading": "New report", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index cb7e1b5a71e..eaa01638ca4 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -18,6 +18,12 @@ "account.unfollow": "Ne plus suivre", "account.unmute": "Ne plus masquer", "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.community": "Fil public local", "column.favourites": "Favoris", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Ne pas afficher dans les fils publics", "privacy.unlisted.short": "Non-listé", "reply_indicator.cancel": "Annuler", - "report.heading": "Nouveau signalement", "report.placeholder": "Commentaires additionnels", "report.submit": "Envoyer", "report.target": "Signalement", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index db3d0039483..98c7ea02136 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -18,6 +18,12 @@ "account.unfollow": "הפסקת מעקב", "account.unmute": "הפסקת השתקת @{name}", "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.community": "ציר זמן מקומי", "column.favourites": "חיבובים", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "לא יופיע בפידים הציבוריים המשותפים", "privacy.unlisted.short": "לא לפיד הכללי", "reply_indicator.cancel": "ביטול", - "report.heading": "דווח חדש", "report.placeholder": "הערות נוספות", "report.submit": "שליחה", "report.target": "דיווח", diff --git a/app/javascript/mastodon/locales/hr.json b/app/javascript/mastodon/locales/hr.json index f85eb8a3f7d..fdf5c11c01f 100644 --- a/app/javascript/mastodon/locales/hr.json +++ b/app/javascript/mastodon/locales/hr.json @@ -18,6 +18,12 @@ "account.unfollow": "Prestani slijediti", "account.unmute": "Poništi utišavanje @{name}", "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.community": "Lokalni timeline", "column.favourites": "Favoriti", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Ne prikazuj u javnim timelineovima", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Otkaži", - "report.heading": "Nova prijava", "report.placeholder": "Dodatni komentari", "report.submit": "Pošalji", "report.target": "Prijavljivanje", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 350410c4b0d..baf762c8dd2 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -18,6 +18,12 @@ "account.unfollow": "Követés abbahagyása", "account.unmute": "Unmute @{name}", "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.community": "Local timeline", "column.favourites": "Favourites", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Do not show in public timelines", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Mégsem", - "report.heading": "New report", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/id.json b/app/javascript/mastodon/locales/id.json index 6e9bc5ba904..6f6d688e9fe 100644 --- a/app/javascript/mastodon/locales/id.json +++ b/app/javascript/mastodon/locales/id.json @@ -18,6 +18,12 @@ "account.unfollow": "Berhenti mengikuti", "account.unmute": "Berhenti membisukan @{name}", "boost_modal.combo": "Anda dapat menekan {combo} untuk melewati ini", + "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": "Pengguna diblokir", "column.community": "Linimasa Lokal", "column.favourites": "Favorit", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Tidak ditampilkan di linimasa publik", "privacy.unlisted.short": "Tak Terdaftar", "reply_indicator.cancel": "Batal", - "report.heading": "Laporan baru", "report.placeholder": "Komentar tambahan", "report.submit": "Kirim", "report.target": "Melaporkan", diff --git a/app/javascript/mastodon/locales/io.json b/app/javascript/mastodon/locales/io.json index 005dd4f56a5..25e0adc8a83 100644 --- a/app/javascript/mastodon/locales/io.json +++ b/app/javascript/mastodon/locales/io.json @@ -18,6 +18,12 @@ "account.unfollow": "Ne plus sequar", "account.unmute": "Ne plus celar @{name}", "boost_modal.combo": "Tu povas presar sur {combo} por omisar co en la venonta foyo", + "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": "Blokusita uzeri", "column.community": "Lokala tempolineo", "column.favourites": "Favorati", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Ne montrar en publika tempolinei", "privacy.unlisted.short": "Ne enlistigota", "reply_indicator.cancel": "Nihiligar", - "report.heading": "Nova denunco", "report.placeholder": "Plusa komenti", "report.submit": "Sendar", "report.target": "Denuncante", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 4a5b218e892..4881b0f08eb 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -18,6 +18,12 @@ "account.unfollow": "Non seguire", "account.unmute": "Non silenziare @{name}", "boost_modal.combo": "Puoi premere {combo} per saltare questo passaggio la prossima volta", + "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": "Utenti bloccati", "column.community": "Timeline locale", "column.favourites": "Apprezzati", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Non mostrare sulla timeline pubblica", "privacy.unlisted.short": "Non elencato", "reply_indicator.cancel": "Annulla", - "report.heading": "Nuova segnalazione", "report.placeholder": "Commenti aggiuntivi", "report.submit": "Invia", "report.target": "Invio la segnalazione", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index cb8074b5de6..a133e633016 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -18,6 +18,12 @@ "account.unfollow": "フォロー解除", "account.unmute": "ミュート解除", "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.community": "ローカルタイムライン", "column.favourites": "お気に入り", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "公開TLで表示しない", "privacy.unlisted.short": "未収載", "reply_indicator.cancel": "キャンセル", - "report.heading": "新規通報", "report.placeholder": "コメント", "report.submit": "通報する", "report.target": "問題のユーザー", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index e88d4a53180..5e1aaac85e2 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -18,6 +18,12 @@ "account.unfollow": "팔로우 해제", "account.unmute": "뮤트 해제", "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.community": "로컬 타임라인", "column.favourites": "즐겨찾기", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "공개 타임라인에 표시하지 않음", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "취소", - "report.heading": "신고", "report.placeholder": "코멘트", "report.submit": "신고하기", "report.target": "문제가 된 사용자", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 05a9e3a12e7..479d157f31e 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -18,6 +18,12 @@ "account.unfollow": "Ontvolgen", "account.unmute": "@{name} niet meer negeren", "boost_modal.combo": "Je kunt {combo} klikken om dit de volgende keer over te slaan", + "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": "Geblokkeerde gebruikers", "column.community": "Lokale tijdlijn", "column.favourites": "Favorieten", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Niet op openbare tijdlijnen tonen", "privacy.unlisted.short": "Minder openbaar", "reply_indicator.cancel": "Annuleren", - "report.heading": "Rapporteren", "report.placeholder": "Extra opmerkingen", "report.submit": "Verzenden", "report.target": "Rapporteren van", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index a3c9562798d..4bbf1493827 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -18,6 +18,12 @@ "account.unfollow": "Avfølg", "account.unmute": "Avdemp @{name}", "boost_modal.combo": "You kan trykke {combo} for å hoppe over dette neste gang", + "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": "Blokkerte brukere", "column.community": "Lokal tidslinje", "column.favourites": "Likt", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Ikke vis i offentlige tidslinjer", "privacy.unlisted.short": "Uoppført", "reply_indicator.cancel": "Avbryt", - "report.heading": "Ny rapport", "report.placeholder": "Tilleggskommentarer", "report.submit": "Send inn", "report.target": "Rapporterer", diff --git a/app/javascript/mastodon/locales/oc.json b/app/javascript/mastodon/locales/oc.json index 5f226bc70a2..2c119ef413a 100644 --- a/app/javascript/mastodon/locales/oc.json +++ b/app/javascript/mastodon/locales/oc.json @@ -18,6 +18,12 @@ "account.unfollow": "Quitar de sègre", "account.unmute": "Quitar de rescondre @{name}", "boost_modal.combo": "Podètz botar {combo} per passar aquò lo còp que ven", + "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": "Personas blocadas", "column.community": "Flux d’actualitat public local", "column.favourites": "Favorits", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Mostrar pas dins los fluxes publics", "privacy.unlisted.short": "Pas-listat", "reply_indicator.cancel": "Anullar", - "report.heading": "Senhalar {target}", "report.placeholder": "Comentaris addicionals", "report.submit": "Mandar", "report.target": "Senhalar {target}", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 2a69824ee2e..b547a173704 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -18,6 +18,12 @@ "account.unfollow": "Przestań obserwować", "account.unmute": "Cofnij wyciszenie @{name}", "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem", + "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": "Zablokowani użytkownicy", "column.community": "Lokalna oś czasu", "column.favourites": "Ulubione", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Niewidoczne na publicznych osiach czasu", "privacy.unlisted.short": "Niewidoczne", "reply_indicator.cancel": "Anuluj", - "report.heading": "Zgłoś {target}", "report.placeholder": "Dodatkowe komentarze", "report.submit": "Wyślij", "report.target": "Zgłaszanie {target}", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 5e5834a0e84..b199a39ce7e 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -18,6 +18,12 @@ "account.unfollow": "Deixar de seguir", "account.unmute": "Não silenciar @{name}", "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", + "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": "Utilizadores Bloqueados", "column.community": "Local", "column.favourites": "Favoritos", @@ -72,7 +78,6 @@ "getting_started.faq": "FAQ", "getting_started.heading": "Primeiros passos", "getting_started.open_source_notice": "Mastodon é software de fonte aberta. Podes contribuir ou repostar problemas no GitHub do projecto: {github}.", - "getting_started.support": "{faq} • {userguide} • {apps}", "getting_started.userguide": "User Guide", "home.column_settings.advanced": "Avançado", "home.column_settings.basic": "Básico", @@ -107,7 +112,6 @@ "notifications.column_settings.reblog": "Partilhas:", "notifications.column_settings.show": "Mostrar nas colunas", "notifications.column_settings.sound": "Reproduzir som", - "notifications.settings": "Parâmetros da listagem de Notificações", "onboarding.done": "Done", "onboarding.next": "Next", "onboarding.page_five.public_timelines": "The local timeline shows public posts from everyone on {domain}. The federated timeline shows public posts from everyone who people on {domain} follow. These are the Public Timelines, a great way to discover new people.", @@ -138,7 +142,6 @@ "privacy.unlisted.long": "Não publicar nos feeds públicos", "privacy.unlisted.short": "Não listar", "reply_indicator.cancel": "Cancelar", - "report.heading": "Nova denúncia", "report.placeholder": "Comentários adicionais", "report.submit": "Enviar", "report.target": "Denunciar", diff --git a/app/javascript/mastodon/locales/pt.json b/app/javascript/mastodon/locales/pt.json index 4ebfe0c60e2..b199a39ce7e 100644 --- a/app/javascript/mastodon/locales/pt.json +++ b/app/javascript/mastodon/locales/pt.json @@ -18,6 +18,12 @@ "account.unfollow": "Deixar de seguir", "account.unmute": "Não silenciar @{name}", "boost_modal.combo": "Pode clicar {combo} para não voltar a ver", + "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": "Utilizadores Bloqueados", "column.community": "Local", "column.favourites": "Favoritos", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Não publicar nos feeds públicos", "privacy.unlisted.short": "Não listar", "reply_indicator.cancel": "Cancelar", - "report.heading": "Nova denúncia", "report.placeholder": "Comentários adicionais", "report.submit": "Enviar", "report.target": "Denunciar", diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json index f561f015172..f9f48a48d98 100644 --- a/app/javascript/mastodon/locales/ru.json +++ b/app/javascript/mastodon/locales/ru.json @@ -18,6 +18,12 @@ "account.unfollow": "Отписаться", "account.unmute": "Снять глушение", "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.community": "Локальная лента", "column.favourites": "Понравившееся", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Не показывать в лентах", "privacy.unlisted.short": "Скрытый", "reply_indicator.cancel": "Отмена", - "report.heading": "Новая жалоба", "report.placeholder": "Комментарий", "report.submit": "Отправить", "report.target": "Жалуемся на", diff --git a/app/javascript/mastodon/locales/th.json b/app/javascript/mastodon/locales/th.json index 608d911e942..8a39beacb7e 100644 --- a/app/javascript/mastodon/locales/th.json +++ b/app/javascript/mastodon/locales/th.json @@ -18,6 +18,12 @@ "account.unfollow": "Unfollow", "account.unmute": "Unmute @{name}", "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.community": "Local timeline", "column.favourites": "Favourites", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Do not post to public timelines", "privacy.unlisted.short": "Unlisted", "reply_indicator.cancel": "Cancel", - "report.heading": "New report", "report.placeholder": "Additional comments", "report.submit": "Submit", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 7512971f5e8..203e4a09e1f 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -18,6 +18,12 @@ "account.unfollow": "Takipten vazgeç", "account.unmute": "Sesi aç @{name}", "boost_modal.combo": "Bir dahaki sefere {combo} tuşuna basabilirsiniz", + "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": "Engellenen kullanıcılar", "column.community": "Yerel zaman tüneli", "column.favourites": "Favoriler", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Herkese açık zaman tüneline gönderme", "privacy.unlisted.short": "Listelenmemiş", "reply_indicator.cancel": "İptal", - "report.heading": "Yeni rapor", "report.placeholder": "Ek yorumlar", "report.submit": "Gönder", "report.target": "Raporlama", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index a117c854b99..c0f4a8dbbd3 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -18,6 +18,12 @@ "account.unfollow": "Відписатися", "account.unmute": "Зняти глушення", "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.community": "Локальна стрічка", "column.favourites": "Вподобане", @@ -136,7 +142,6 @@ "privacy.unlisted.long": "Не показувати у публічних стрічках", "privacy.unlisted.short": "Прихований", "reply_indicator.cancel": "Відмінити", - "report.heading": "Нова скарга", "report.placeholder": "Додаткові коментарі", "report.submit": "Відправити", "report.target": "Скаржимося на", diff --git a/app/javascript/mastodon/locales/whitelist_zh-HK.json b/app/javascript/mastodon/locales/whitelist_zh-HK.json index d5fddc3bcb1..0d4f101c7a3 100644 --- a/app/javascript/mastodon/locales/whitelist_zh-HK.json +++ b/app/javascript/mastodon/locales/whitelist_zh-HK.json @@ -1,3 +1,2 @@ [ - "getting_started.support" ] diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 68648f2dd37..998e1c8dac8 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -18,6 +18,12 @@ "account.unfollow": "取消关注", "account.unmute": "取消 @{name} 的静音", "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.community": "本站时间轴", "column.favourites": "赞过的嘟文", @@ -72,7 +78,6 @@ "getting_started.faq": "FAQ", "getting_started.heading": "开始使用", "getting_started.open_source_notice": "Mastodon 是一个开放源码的软件。你可以在官方 GitHub ({github}) 贡献或者回报问题。", - "getting_started.support": "{faq} • {userguide} • {apps}", "getting_started.userguide": "User Guide", "home.column_settings.advanced": "高端", "home.column_settings.basic": "基本", @@ -107,7 +112,6 @@ "notifications.column_settings.reblog": "你的嘟文被转嘟:", "notifications.column_settings.show": "在通知栏显示", "notifications.column_settings.sound": "播放音效", - "notifications.settings": "字段设置", "onboarding.done": "出发!", "onboarding.next": "下一步", "onboarding.page_five.public_timelines": "本站时间轴显示来自 {domain} 的所有人的公共嘟文。 跨站公共时间轴显示 {domain} 上的各位关注的来自所有Mastodon服务器实例上的人发表的公共嘟文。这些就是寻人好去处的公共时间轴啦。", @@ -138,7 +142,6 @@ "privacy.unlisted.long": "公开,但不在公共时间轴显示", "privacy.unlisted.short": "公开", "reply_indicator.cancel": "取消", - "report.heading": "举报", "report.placeholder": "额外消息", "report.submit": "提交", "report.target": "Reporting", diff --git a/app/javascript/mastodon/locales/zh-HK.json b/app/javascript/mastodon/locales/zh-HK.json index a9844a62590..1079d54295b 100644 --- a/app/javascript/mastodon/locales/zh-HK.json +++ b/app/javascript/mastodon/locales/zh-HK.json @@ -18,6 +18,12 @@ "account.unfollow": "取消關注", "account.unmute": "取消 @{name} 的靜音", "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.community": "本站時間軸", "column.favourites": "喜歡的文章", @@ -72,7 +78,6 @@ "getting_started.faq": "常見問題", "getting_started.heading": "開始使用", "getting_started.open_source_notice": "Mastodon(萬象)是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。", - "getting_started.support": "{faq} • {userguide} • {apps}", "getting_started.userguide": "使用指南", "home.column_settings.advanced": "進階", "home.column_settings.basic": "基本", @@ -107,7 +112,6 @@ "notifications.column_settings.reblog": "轉推你的文章:", "notifications.column_settings.show": "在通知欄顯示", "notifications.column_settings.sound": "播放音效", - "notifications.settings": "欄位設定", "onboarding.done": "開始使用", "onboarding.next": "繼續", "onboarding.page_five.public_timelines": "「本站時間軸」顯示在 {domain} 各用戶的公開文章。「跨站時間軸」顯示在 {domain} 各人關注的所有用戶(包括其他服務站)的公開文章。這些都是「公共時間軸」,是認識新朋友的好地方。", @@ -138,7 +142,6 @@ "privacy.unlisted.long": "公開,但不在公共時間軸顯示", "privacy.unlisted.short": "公開", "reply_indicator.cancel": "取消", - "report.heading": "舉報", "report.placeholder": "額外訊息", "report.submit": "提交", "report.target": "舉報", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 5497becf035..6240b887914 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -18,6 +18,12 @@ "account.unfollow": "取消關注", "account.unmute": "不再消音 @{name}", "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.community": "本地時間軸", "column.favourites": "最愛", @@ -72,7 +78,6 @@ "getting_started.faq": "FAQ", "getting_started.heading": "馬上開始", "getting_started.open_source_notice": "Mastodon 是開源軟體。你可以在 GitHub {github} 上做出貢獻或是回報問題。", - "getting_started.support": "{faq} • {userguide} • {apps}", "getting_started.userguide": "使用者指南", "home.column_settings.advanced": "進階", "home.column_settings.basic": "基本", @@ -107,7 +112,6 @@ "notifications.column_settings.reblog": "轉推:", "notifications.column_settings.show": "顯示在欄位中", "notifications.column_settings.sound": "播放音效", - "notifications.settings": "欄位設定", "onboarding.done": "完成", "onboarding.next": "下一步", "onboarding.page_five.public_timelines": "本地時間軸顯示 {domain} 上所有人的公開貼文。聯盟時間軸顯示 {domain} 上所有人關注的公開貼文。這就是公開時間軸,發現新朋友的好地方。", @@ -138,7 +142,6 @@ "privacy.unlisted.long": "不要貼到公開時間軸", "privacy.unlisted.short": "不列出來", "reply_indicator.cancel": "取消", - "report.heading": "新的通報", "report.placeholder": "更多訊息", "report.submit": "送出", "report.target": "通報中", diff --git a/config/webpack/translationRunner.js b/config/webpack/translationRunner.js index 097099b48cb..d616c78391e 100644 --- a/config/webpack/translationRunner.js +++ b/config/webpack/translationRunner.js @@ -2,7 +2,7 @@ const fs = require('fs'); const path = require('path'); const { default: manageTranslations } = require('react-intl-translations-manager'); -const RFC5646_REGEXP = /^[a-z]{2,3}(?:|[A-Z]+)$/; +const RFC5646_REGEXP = /^[a-z]{2,3}(?:|-[A-Z]+)$/; const rootDirectory = path.resolve(__dirname, '..', '..'); const translationsDirectory = path.resolve(rootDirectory, 'app', 'javascript', 'mastodon', 'locales'); From ca45bd0361fa1d182a505cf2eff1a69bc7712a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AE=E3=82=89?= Date: Mon, 10 Jul 2017 21:04:05 +0900 Subject: [PATCH 093/114] Add Japanese translation of terms and flash (#4137) --- app/controllers/admin/settings_controller.rb | 2 +- config/locales/ja.yml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 7542f55e8b3..f27a1f4d450 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -24,7 +24,7 @@ module Admin setting.update(value: value_for_update(key, value)) end - flash[:notice] = 'Success!' + flash[:notice] = I18n.t('generic.changes_saved_msg') redirect_to edit_admin_settings_path end diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 347270af243..9cb2428f70c 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -183,6 +183,9 @@ ja: site_description_extended: desc_html: インスタンスについてのページに表示されます。
HTMLタグが利用可能です。 title: サイトの詳細な説明 + site_terms: + desc_html: プライバシーポリシーのページに表示されます。
HTMLタグが利用可能です。 + title: サイトのプライバシーポリシー site_title: サイトのタイトル title: サイト設定 subscriptions: From 31490e0d6ce5f967146da7f7bafa73a1ecb65fb9 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 10 Jul 2017 23:32:17 +0900 Subject: [PATCH 094/114] Add Japanese translations (#4140) * Add Japanese translations for #3879 * Add Japanese translations for #4033 * Add Japanese translations for #4136 --- app/javascript/mastodon/locales/ja.json | 12 ++++++------ config/locales/simple_form.ja.yml | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index a133e633016..f6207285228 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -18,12 +18,12 @@ "account.unfollow": "フォロー解除", "account.unmute": "ミュート解除", "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", + "bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。", + "bundle_column_error.retry": "再試行", + "bundle_column_error.title": "ネットワークエラー", + "bundle_modal_error.close": "閉じる", + "bundle_modal_error.message": "コンポーネントの読み込み中に問題が発生しました。", + "bundle_modal_error.retry": "再試行", "column.blocks": "ブロックしたユーザー", "column.community": "ローカルタイムライン", "column.favourites": "お気に入り", diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml index b9f11d7b389..9342398a8c9 100644 --- a/config/locales/simple_form.ja.yml +++ b/config/locales/simple_form.ja.yml @@ -34,7 +34,9 @@ ja: setting_auto_play_gif: アニメーションGIFを自動再生する setting_boost_modal: ブーストする前に確認ダイアログを表示する setting_default_privacy: 投稿の公開範囲 + setting_default_sensitive: メディアを常に閲覧注意としてマークする setting_delete_modal: トゥートを削除する前に確認ダイアログを表示する + setting_system_font_ui: システムのデフォルトフォントを使う severity: 重大性 type: インポートする項目 username: ユーザー名 From 7f9a353b94f628396c445ae07ed1508c91b93fcb Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Mon, 10 Jul 2017 18:04:06 +0200 Subject: [PATCH 095/114] i18n: @63baab0 (pl) (#4141) --- app/javascript/mastodon/locales/pl.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index b547a173704..ac63ec40fe1 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -18,12 +18,12 @@ "account.unfollow": "Przestań obserwować", "account.unmute": "Cofnij wyciszenie @{name}", "boost_modal.combo": "Naciśnij {combo}, aby pominąć to następnym razem", - "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", + "bundle_column_error.body": "Coś poszło nie tak podczas ładowania tego składnika.", + "bundle_column_error.retry": "Spróbuj ponownie", + "bundle_column_error.title": "Błąd sieci", + "bundle_modal_error.close": "Zamknij", + "bundle_modal_error.message": "Coś poszło nie tak podczas ładowania tego składnika.", + "bundle_modal_error.retry": "Spróbuj ponownie", "column.blocks": "Zablokowani użytkownicy", "column.community": "Lokalna oś czasu", "column.favourites": "Ulubione", From 34ccc058fa738cd73c273d6b64efbe67402bd86c Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 10 Jul 2017 18:04:23 +0200 Subject: [PATCH 096/114] Limit total subscribe retries to 10, but space them out more (#4142) Since there is little point in retrying so often when a service is down or does not exist anymore. Subscriptions are renewed 1 day before they should expire, so retrying in 30 minutes, then 2 hours, then 12 hours is fine. If even after that, the remote server does not work, there is little sense in retrying more often than once a day Also, uniqueness of the job should ensure that failed retries will not result in multiple retries for the same endpoint when the next resubscription cycle comes --- app/workers/pubsubhubbub/subscribe_worker.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/workers/pubsubhubbub/subscribe_worker.rb b/app/workers/pubsubhubbub/subscribe_worker.rb index 5b0956b6bb9..6865e71360b 100644 --- a/app/workers/pubsubhubbub/subscribe_worker.rb +++ b/app/workers/pubsubhubbub/subscribe_worker.rb @@ -3,7 +3,20 @@ class Pubsubhubbub::SubscribeWorker include Sidekiq::Worker - sidekiq_options queue: 'push' + sidekiq_options queue: 'push', retry: 10, unique: :until_executed + + sidekiq_retry_in do |count| + case count + when 0 + 30.minutes.seconds + when 1 + 2.hours.seconds + when 2 + 12.hours.seconds + else + 24.hours.seconds * (count - 2) + end + end def perform(account_id) account = Account.find(account_id) From d081d4a4226ba317a1b653aa6eef57543cbe4430 Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Mon, 10 Jul 2017 18:04:43 +0200 Subject: [PATCH 097/114] i18n: @2b9721d (pl) (#4143) --- config/locales/simple_form.pl.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index 2a3756d4d39..e4c4d7c8c9a 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -42,6 +42,7 @@ pl: setting_auto_play_gif: Automatycznie odtwarzaj animowane GIFy setting_boost_modal: Pytaj o potwierdzenie przed podbiciem setting_default_privacy: Widoczność posta + setting_default_sensitive: Zawsze oznaczaj zawartość multimedialną jako wrażliwą setting_delete_modal: Pytaj o potwierdzenie przed usunięciem postu setting_system_font_ui: Używaj domyślnej czcionki systemu severity: Priorytet From 7a889a8e125a03e109b225cd83b0abcbdc76d95b Mon Sep 17 00:00:00 2001 From: STJrInuyasha Date: Mon, 10 Jul 2017 09:05:06 -0700 Subject: [PATCH 098/114] Remote following success page (#4129) * Added a success page to remote following Includes follow-through links to web (the old redirect target) and back to the remote user's profile * Use Account.new in spec instead of a fake with only id (fixes spec) * Fabricate(:account) over Account.new * Remove self from the success text (and all HTML with it) --- app/controllers/authorize_follows_controller.rb | 2 +- app/javascript/styles/forms.scss | 9 +++++++++ app/views/authorize_follows/success.html.haml | 16 ++++++++++++++++ config/locales/en.yml | 6 ++++++ .../authorize_follows_controller_spec.rb | 4 ++-- 5 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 app/views/authorize_follows/success.html.haml diff --git a/app/controllers/authorize_follows_controller.rb b/app/controllers/authorize_follows_controller.rb index da4ef022a0b..dccd1c20908 100644 --- a/app/controllers/authorize_follows_controller.rb +++ b/app/controllers/authorize_follows_controller.rb @@ -15,7 +15,7 @@ class AuthorizeFollowsController < ApplicationController if @account.nil? render :error else - redirect_to web_url("accounts/#{@account.id}") + render :success end rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError render :error diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss index 7a181f36b55..414dc4fe872 100644 --- a/app/javascript/styles/forms.scss +++ b/app/javascript/styles/forms.scss @@ -375,3 +375,12 @@ code { width: 50%; } } + +.post-follow-actions { + text-align: center; + color: $ui-primary-color; + + div { + margin-bottom: 4px; + } +} diff --git a/app/views/authorize_follows/success.html.haml b/app/views/authorize_follows/success.html.haml new file mode 100644 index 00000000000..f0b495689b8 --- /dev/null +++ b/app/views/authorize_follows/success.html.haml @@ -0,0 +1,16 @@ +- content_for :page_title do + = t('authorize_follow.title', acct: @account.acct) + +.form-container + .follow-prompt + - if @account.locked? + %h2= t('authorize_follow.follow_request') + - else + %h2= t('authorize_follow.following') + + = render 'card', account: @account + + .post-follow-actions + %div= link_to t('authorize_follow.post_follow.web'), web_url("accounts/#{@account.id}"), class: 'button button--block' + %div= link_to t('authorize_follow.post_follow.return'), @account.url, class: 'button button--block' + %div= t('authorize_follow.post_follow.close') diff --git a/config/locales/en.yml b/config/locales/en.yml index 60e192491e1..8bb893d1c33 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -221,6 +221,12 @@ en: authorize_follow: error: Unfortunately, there was an error looking up the remote account follow: Follow + following: 'Success! You are now following:' + follow_request: 'You have sent a follow request to:' + post_follow: + web: Go to web + return: Return to the user's profile + close: Or, you can just close this window. prompt_html: 'You (%{self}) have requested to follow:' title: Follow %{acct} datetime: diff --git a/spec/controllers/authorize_follows_controller_spec.rb b/spec/controllers/authorize_follows_controller_spec.rb index b801aa66174..26e46a23c2d 100644 --- a/spec/controllers/authorize_follows_controller_spec.rb +++ b/spec/controllers/authorize_follows_controller_spec.rb @@ -94,7 +94,7 @@ describe AuthorizeFollowsController do end it 'follows account when found' do - target_account = double(id: '123') + target_account = Fabricate(:account) result_account = double(target_account: target_account) service = double allow(FollowService).to receive(:new).and_return(service) @@ -103,7 +103,7 @@ describe AuthorizeFollowsController do post :create, params: { acct: 'acct:user@hostname' } expect(service).to have_received(:call).with(account, 'user@hostname') - expect(response).to redirect_to(web_url('accounts/123')) + expect(response).to render_template(:success) end end end From a3d93e8bbede04ddbbab10168360a32aedc6bcf7 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 10 Jul 2017 18:46:46 +0200 Subject: [PATCH 099/114] Fix #4059 - Remove ModuleConcatenationPlugin (#4139) It increased memory usage of Webpack 1.5x fold with little benefits --- config/webpack/production.js | 1 - 1 file changed, 1 deletion(-) diff --git a/config/webpack/production.js b/config/webpack/production.js index 0d2c9acfb44..303fca81b27 100644 --- a/config/webpack/production.js +++ b/config/webpack/production.js @@ -12,7 +12,6 @@ module.exports = merge(sharedConfig, { stats: 'normal', plugins: [ - new webpack.optimize.ModuleConcatenationPlugin(), new webpack.optimize.UglifyJsPlugin({ sourceMap: true, mangle: true, From e670fa2af6420268960129d04406db23c3554570 Mon Sep 17 00:00:00 2001 From: unarist Date: Tue, 11 Jul 2017 02:41:55 +0900 Subject: [PATCH 100/114] Update es5-ext to avoid CSP violation (#4145) Since es5-ext used `new Function("...")`, it caused CSP violation unless "unsafe-eval" included. So this patch updates it to the version which fixes it. Note that this package is used in polyfills, so loaded only if needed. I've encountered this issue on iOS9. cf. medikoo/es5-ext@d3864493 --- yarn.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/yarn.lock b/yarn.lock index aedbde6be61..fba802e0a26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2512,8 +2512,8 @@ es-to-primitive@^1.1.1: is-symbol "^1.0.1" es5-ext@^0.10.14, es5-ext@^0.10.9, es5-ext@~0.10.14: - version "0.10.23" - resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.23.tgz#7578b51be974207a5487821b56538c224e4e7b38" + version "0.10.24" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.24.tgz#a55877c9924bc0c8d9bd3c2cbe17495ac1709b14" dependencies: es6-iterator "2" es6-symbol "~3.1" From 958fe0f7db6702d65791def89fceef77d6f43589 Mon Sep 17 00:00:00 2001 From: m4sk1n Date: Mon, 10 Jul 2017 19:42:37 +0200 Subject: [PATCH 101/114] i18n: @7a889a8 (pl) (#4144) * i18n: @7a889a8 (pl) * Update pl.yml --- config/locales/pl.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 0dc1da8b44e..321b1590eae 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -221,6 +221,12 @@ pl: authorize_follow: error: Niestety, podczas sprawdzania zdalnego konta wystąpił błąd follow: Śledź + following: 'Pomyślnie! Od teraz śledzisz:' + follow_request: 'Wysłano prośbę o pozwolenie na obserwację:' + post_follow: + web: Przejdź do sieci + return: Powróć do strony użytkownika + close: Ewentualnie, możesz po prostu zamknąć tą stronę. prompt_html: 'Ty (%{self}) chcesz śledzić:' title: Śledź %{acct} datetime: From 7bacdd718a143f54f47ddc3afa39504636be65c0 Mon Sep 17 00:00:00 2001 From: "Akihiko Odaki (@fn_aki@pawoo.net)" Date: Tue, 11 Jul 2017 08:00:01 +0900 Subject: [PATCH 102/114] Fix PrecomputeFeedService for filtered statuses (#4148) --- app/services/precompute_feed_service.rb | 4 ++-- spec/services/precompute_feed_service_spec.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/services/precompute_feed_service.rb b/app/services/precompute_feed_service.rb index a32ba1dae4f..85635a0082e 100644 --- a/app/services/precompute_feed_service.rb +++ b/app/services/precompute_feed_service.rb @@ -13,7 +13,7 @@ class PrecomputeFeedService < BaseService attr_reader :account def populate_feed - pairs = statuses.reverse_each.map(&method(:process_status)) + pairs = statuses.reverse_each.lazy.reject(&method(:status_filtered?)).map(&method(:process_status)).to_a redis.pipelined do redis.zadd(account_home_key, pairs) if pairs.any? @@ -22,7 +22,7 @@ class PrecomputeFeedService < BaseService end def process_status(status) - [status.id, status.reblog? ? status.reblog_of_id : status.id] unless status_filtered?(status) + [status.id, status.reblog? ? status.reblog_of_id : status.id] end def status_filtered?(status) diff --git a/spec/services/precompute_feed_service_spec.rb b/spec/services/precompute_feed_service_spec.rb index e2294469c03..dbd08ac1b0f 100644 --- a/spec/services/precompute_feed_service_spec.rb +++ b/spec/services/precompute_feed_service_spec.rb @@ -23,5 +23,17 @@ RSpec.describe PrecomputeFeedService do account = Fabricate(:account) subject.call(account) end + + it 'filters statuses' do + account = Fabricate(:account) + muted_account = Fabricate(:account) + Fabricate(:mute, account: account, target_account: muted_account) + reblog = Fabricate(:status, account: muted_account) + status = Fabricate(:status, account: account, reblog: reblog) + + subject.call(account) + + expect(Redis.current.zscore(FeedManager.instance.key(:home, account.id), reblog.id)).to eq nil + end end end From cc68d1945b42459a787e0ce216e8f49393eeb197 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Tue, 11 Jul 2017 01:00:14 +0200 Subject: [PATCH 103/114] refactor: Rewrite immutablejs import statements using destructuring (#4147) --- .../mastodon/actions/notifications.js | 4 +-- app/javascript/mastodon/actions/store.js | 6 ++-- app/javascript/mastodon/actions/timelines.js | 10 +++--- .../features/account_timeline/index.js | 4 +-- .../mastodon/features/notifications/index.js | 4 +-- .../containers/status_check_box_container.js | 4 +-- .../ui/components/onboarding_modal.js | 4 +-- .../features/ui/components/report_modal.js | 4 +-- .../ui/containers/status_list_container.js | 6 ++-- app/javascript/mastodon/reducers/accounts.js | 6 ++-- .../mastodon/reducers/accounts_counters.js | 8 ++--- app/javascript/mastodon/reducers/alerts.js | 6 ++-- app/javascript/mastodon/reducers/cards.js | 6 ++-- app/javascript/mastodon/reducers/compose.js | 18 +++++------ app/javascript/mastodon/reducers/contexts.js | 18 +++++------ .../mastodon/reducers/media_attachments.js | 4 +-- app/javascript/mastodon/reducers/meta.js | 4 +-- .../mastodon/reducers/notifications.js | 14 ++++---- .../mastodon/reducers/relationships.js | 6 ++-- app/javascript/mastodon/reducers/reports.js | 16 +++++----- app/javascript/mastodon/reducers/search.js | 16 +++++----- app/javascript/mastodon/reducers/settings.js | 30 ++++++++--------- .../mastodon/reducers/status_lists.js | 10 +++--- app/javascript/mastodon/reducers/statuses.js | 6 ++-- app/javascript/mastodon/reducers/timelines.js | 24 +++++++------- .../mastodon/reducers/user_lists.js | 32 +++++++++---------- app/javascript/mastodon/selectors/index.js | 6 ++-- .../components/display_name.test.js | 6 ++-- 28 files changed, 141 insertions(+), 141 deletions(-) diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index cda636139d0..c7d24812273 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -1,5 +1,5 @@ import api, { getLinks } from '../api'; -import Immutable from 'immutable'; +import { List as ImmutableList } from 'immutable'; import IntlMessageFormat from 'intl-messageformat'; import { fetchRelationships } from './accounts'; import { defineMessages } from 'react-intl'; @@ -124,7 +124,7 @@ export function refreshNotificationsFail(error, skipLoading) { export function expandNotifications() { 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) { return; diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js index efdb0771a5a..0597d265ec4 100644 --- a/app/javascript/mastodon/actions/store.js +++ b/app/javascript/mastodon/actions/store.js @@ -1,11 +1,11 @@ -import Immutable from 'immutable'; +import { Iterable, fromJS } from 'immutable'; export const STORE_HYDRATE = 'STORE_HYDRATE'; export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; const convertState = rawState => - Immutable.fromJS(rawState, (k, v) => - Immutable.Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x => + fromJS(rawState, (k, v) => + Iterable.isIndexed(v) ? v.toList() : v.toMap().mapKeys(x => Number.isNaN(x * 1) ? x : x * 1)); export function hydrateStore(rawState) { diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index cb4410eba80..dd14cb1cd03 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -1,5 +1,5 @@ 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_DELETE = 'TIMELINE_DELETE'; @@ -66,13 +66,13 @@ export function refreshTimelineRequest(timeline, skipLoading) { export function refreshTimeline(timelineId, path, params = {}) { 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')) { return; } - const ids = timeline.get('items', Immutable.List()); + const ids = timeline.get('items', ImmutableList()); const newestId = ids.size > 0 ? ids.first() : null; let skipLoading = timeline.get('loaded'); @@ -111,8 +111,8 @@ export function refreshTimelineFail(timeline, error, skipLoading) { export function expandTimeline(timelineId, path, params = {}) { return (dispatch, getState) => { - const timeline = getState().getIn(['timelines', timelineId], Immutable.Map()); - const ids = timeline.get('items', Immutable.List()); + const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); + const ids = timeline.get('items', ImmutableList()); if (timeline.get('isLoading') || ids.size === 0) { return; diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index 955d0000e7a..3c8b63114f6 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -9,11 +9,11 @@ import LoadingIndicator from '../../components/loading_indicator'; import Column from '../ui/components/column'; import HeaderContainer from './containers/header_container'; import ColumnBackButton from '../../components/column_back_button'; -import Immutable from 'immutable'; +import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; 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']), hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'next']), me: state.getIn(['meta', 'me']), diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 2f545fa4a32..c5853d3bad2 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -11,7 +11,7 @@ import { ScrollContainer } from 'react-router-scroll'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import ColumnSettingsContainer from './containers/column_settings_container'; import { createSelector } from 'reselect'; -import Immutable from 'immutable'; +import { List as ImmutableList } from 'immutable'; import LoadMore from '../../components/load_more'; import { debounce } from 'lodash'; @@ -20,7 +20,7 @@ const messages = defineMessages({ }); 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']), ], (excludedTypes, notifications) => notifications.filterNot(item => excludedTypes.includes(item.get('type')))); diff --git a/app/javascript/mastodon/features/report/containers/status_check_box_container.js b/app/javascript/mastodon/features/report/containers/status_check_box_container.js index 8997718a2aa..48cd0319bdf 100644 --- a/app/javascript/mastodon/features/report/containers/status_check_box_container.js +++ b/app/javascript/mastodon/features/report/containers/status_check_box_container.js @@ -1,11 +1,11 @@ import { connect } from 'react-redux'; import StatusCheckBox from '../components/status_check_box'; import { toggleStatusReport } from '../../../actions/reports'; -import Immutable from 'immutable'; +import { Set as ImmutableSet } from 'immutable'; const mapStateToProps = (state, { 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 }) => ({ diff --git a/app/javascript/mastodon/features/ui/components/onboarding_modal.js b/app/javascript/mastodon/features/ui/components/onboarding_modal.js index 189bd86651b..3d59785e2f7 100644 --- a/app/javascript/mastodon/features/ui/components/onboarding_modal.js +++ b/app/javascript/mastodon/features/ui/components/onboarding_modal.js @@ -10,7 +10,7 @@ import ComposeForm from '../../compose/components/compose_form'; import Search from '../../compose/components/search'; import NavigationBar from '../../compose/components/navigation_bar'; import ColumnHeader from './column_header'; -import Immutable from 'immutable'; +import { List as ImmutableList } from 'immutable'; const noop = () => { }; @@ -48,7 +48,7 @@ const PageTwo = ({ me }) => (
{ isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), account: getAccount(state, accountId), 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'])), }; }; diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index 45ad6209b64..1b2e1056a3a 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -1,13 +1,13 @@ import { connect } from 'react-redux'; import StatusList from '../../../components/status_list'; import { scrollTopTimeline } from '../../../actions/timelines'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { createSelector } from 'reselect'; import { debounce } from 'lodash'; const makeGetStatusIds = () => createSelector([ - (state, { type }) => state.getIn(['settings', type], Immutable.Map()), - (state, { type }) => state.getIn(['timelines', type, 'items'], Immutable.List()), + (state, { type }) => state.getIn(['settings', type], ImmutableMap()), + (state, { type }) => state.getIn(['timelines', type, 'items'], ImmutableList()), (state) => state.get('statuses'), (state) => state.getIn(['meta', 'me']), ], (columnSettings, statusIds, statuses, me) => statusIds.filter(id => { diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js index 7b707431731..4d7c3adc972 100644 --- a/app/javascript/mastodon/reducers/accounts.js +++ b/app/javascript/mastodon/reducers/accounts.js @@ -44,7 +44,7 @@ import { FAVOURITED_STATUSES_EXPAND_SUCCESS, } from '../actions/favourites'; import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; const normalizeAccount = (state, account) => { account = { ...account }; @@ -53,7 +53,7 @@ const normalizeAccount = (state, account) => { delete account.following_count; delete account.statuses_count; - return state.set(account.id, Immutable.fromJS(account)); + return state.set(account.id, fromJS(account)); }; const normalizeAccounts = (state, accounts) => { @@ -82,7 +82,7 @@ const normalizeAccountsFromStatuses = (state, statuses) => { return state; }; -const initialState = Immutable.Map(); +const initialState = ImmutableMap(); export default function accounts(state = initialState, action) { switch(action.type) { diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js index eb8a4f83dc1..4423e1b50e7 100644 --- a/app/javascript/mastodon/reducers/accounts_counters.js +++ b/app/javascript/mastodon/reducers/accounts_counters.js @@ -46,9 +46,9 @@ import { FAVOURITED_STATUSES_EXPAND_SUCCESS, } from '../actions/favourites'; import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; -const normalizeAccount = (state, account) => state.set(account.id, Immutable.fromJS({ +const normalizeAccount = (state, account) => state.set(account.id, fromJS({ followers_count: account.followers_count, following_count: account.following_count, statuses_count: account.statuses_count, @@ -80,12 +80,12 @@ const normalizeAccountsFromStatuses = (state, statuses) => { return state; }; -const initialState = Immutable.Map(); +const initialState = ImmutableMap(); export default function accountsCounters(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: - return state.merge(action.state.get('accounts').map(item => Immutable.fromJS({ + return state.merge(action.state.get('accounts').map(item => fromJS({ followers_count: item.get('followers_count'), following_count: item.get('following_count'), statuses_count: item.get('statuses_count'), diff --git a/app/javascript/mastodon/reducers/alerts.js b/app/javascript/mastodon/reducers/alerts.js index aaea9775f37..089d920c3e4 100644 --- a/app/javascript/mastodon/reducers/alerts.js +++ b/app/javascript/mastodon/reducers/alerts.js @@ -3,14 +3,14 @@ import { ALERT_DISMISS, ALERT_CLEAR, } from '../actions/alerts'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -const initialState = Immutable.List([]); +const initialState = ImmutableList([]); export default function alerts(state = initialState, action) { switch(action.type) { case ALERT_SHOW: - return state.push(Immutable.Map({ + return state.push(ImmutableMap({ key: state.size > 0 ? state.last().get('key') + 1 : 0, title: action.title, message: action.message, diff --git a/app/javascript/mastodon/reducers/cards.js b/app/javascript/mastodon/reducers/cards.js index 3c9395011e8..4d86b0d7e7a 100644 --- a/app/javascript/mastodon/reducers/cards.js +++ b/app/javascript/mastodon/reducers/cards.js @@ -1,13 +1,13 @@ import { STATUS_CARD_FETCH_SUCCESS } from '../actions/cards'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; -const initialState = Immutable.Map(); +const initialState = ImmutableMap(); export default function cards(state = initialState, action) { switch(action.type) { case STATUS_CARD_FETCH_SUCCESS: - return state.set(action.id, Immutable.fromJS(action.card)); + return state.set(action.id, fromJS(action.card)); default: return state; } diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 7523777394f..a92b5aa236b 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -24,10 +24,10 @@ import { } from '../actions/compose'; import { TIMELINE_DELETE } from '../actions/timelines'; import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import uuid from '../uuid'; -const initialState = Immutable.Map({ +const initialState = ImmutableMap({ mounted: false, sensitive: false, spoiler: false, @@ -40,9 +40,9 @@ const initialState = Immutable.Map({ is_submitting: false, is_uploading: false, progress: 0, - media_attachments: Immutable.List(), + media_attachments: ImmutableList(), suggestion_token: null, - suggestions: Immutable.List(), + suggestions: ImmutableList(), me: null, default_privacy: 'public', default_sensitive: false, @@ -51,7 +51,7 @@ const initialState = Immutable.Map({ }); function statusToTextMentions(state, status) { - let set = Immutable.OrderedSet([]); + let set = ImmutableOrderedSet([]); let me = state.get('me'); if (status.getIn(['account', 'id']) !== me) { @@ -111,7 +111,7 @@ const insertSuggestion = (state, position, token, completion) => { return state.withMutations(map => { map.update('text', oldText => `${oldText.slice(0, position)}${completion} ${oldText.slice(position + token.length)}`); map.set('suggestion_token', null); - map.update('suggestions', Immutable.List(), list => list.clear()); + map.update('suggestions', ImmutableList(), list => list.clear()); map.set('focusDate', new Date()); map.set('idempotencyKey', uuid()); }); @@ -206,7 +206,7 @@ export default function compose(state = initialState, action) { map.set('is_uploading', true); }); case COMPOSE_UPLOAD_SUCCESS: - return appendMedia(state, Immutable.fromJS(action.media)); + return appendMedia(state, fromJS(action.media)); case COMPOSE_UPLOAD_FAIL: return state.set('is_uploading', false); case COMPOSE_UPLOAD_UNDO: @@ -219,9 +219,9 @@ export default function compose(state = initialState, action) { .set('focusDate', new Date()) .set('idempotencyKey', uuid()); case COMPOSE_SUGGESTIONS_CLEAR: - return state.update('suggestions', Immutable.List(), list => list.clear()).set('suggestion_token', null); + return state.update('suggestions', ImmutableList(), list => list.clear()).set('suggestion_token', null); case COMPOSE_SUGGESTIONS_READY: - return state.set('suggestions', Immutable.List(action.accounts.map(item => item.id))).set('suggestion_token', action.token); + return state.set('suggestions', ImmutableList(action.accounts.map(item => item.id))).set('suggestion_token', action.token); case COMPOSE_SUGGESTION_SELECT: return insertSuggestion(state, action.position, action.token, action.completion); case TIMELINE_DELETE: diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js index 8a24f5f7af6..9bfc09aa757 100644 --- a/app/javascript/mastodon/reducers/contexts.js +++ b/app/javascript/mastodon/reducers/contexts.js @@ -1,10 +1,10 @@ import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; import { TIMELINE_DELETE } from '../actions/timelines'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; -const initialState = Immutable.Map({ - ancestors: Immutable.Map(), - descendants: Immutable.Map(), +const initialState = ImmutableMap({ + ancestors: ImmutableMap(), + descendants: ImmutableMap(), }); const normalizeContext = (state, id, ancestors, descendants) => { @@ -18,12 +18,12 @@ const normalizeContext = (state, id, ancestors, descendants) => { }; const deleteFromContexts = (state, id) => { - state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => { - state = state.updateIn(['ancestors', descendantId], Immutable.List(), list => list.filterNot(itemId => itemId === id)); + state.getIn(['descendants', id], ImmutableList()).forEach(descendantId => { + state = state.updateIn(['ancestors', descendantId], ImmutableList(), list => list.filterNot(itemId => itemId === id)); }); - state.getIn(['ancestors', id], Immutable.List()).forEach(ancestorId => { - state = state.updateIn(['descendants', ancestorId], Immutable.List(), list => list.filterNot(itemId => itemId === id)); + state.getIn(['ancestors', id], ImmutableList()).forEach(ancestorId => { + state = state.updateIn(['descendants', ancestorId], ImmutableList(), list => list.filterNot(itemId => itemId === id)); }); state = state.deleteIn(['descendants', id]).deleteIn(['ancestors', id]); @@ -34,7 +34,7 @@ const deleteFromContexts = (state, id) => { export default function contexts(state = initialState, action) { switch(action.type) { case CONTEXT_FETCH_SUCCESS: - return normalizeContext(state, action.id, Immutable.fromJS(action.ancestors), Immutable.fromJS(action.descendants)); + return normalizeContext(state, action.id, fromJS(action.ancestors), fromJS(action.descendants)); case TIMELINE_DELETE: return deleteFromContexts(state, action.id); default: diff --git a/app/javascript/mastodon/reducers/media_attachments.js b/app/javascript/mastodon/reducers/media_attachments.js index 85bea4f0b38..24119f6286b 100644 --- a/app/javascript/mastodon/reducers/media_attachments.js +++ b/app/javascript/mastodon/reducers/media_attachments.js @@ -1,7 +1,7 @@ import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; +import { Map as ImmutableMap } from 'immutable'; -const initialState = Immutable.Map({ +const initialState = ImmutableMap({ accept_content_types: [], }); diff --git a/app/javascript/mastodon/reducers/meta.js b/app/javascript/mastodon/reducers/meta.js index 1551228ec7b..119ef9d8f89 100644 --- a/app/javascript/mastodon/reducers/meta.js +++ b/app/javascript/mastodon/reducers/meta.js @@ -1,7 +1,7 @@ import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; +import { Map as ImmutableMap } from 'immutable'; -const initialState = Immutable.Map({ +const initialState = ImmutableMap({ streaming_api_base_url: null, access_token: null, me: null, diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 0c1cf5b0f22..0063d24e45b 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -11,10 +11,10 @@ import { } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS } from '../actions/accounts'; import { TIMELINE_DELETE } from '../actions/timelines'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -const initialState = Immutable.Map({ - items: Immutable.List(), +const initialState = ImmutableMap({ + items: ImmutableList(), next: null, top: true, unread: 0, @@ -22,7 +22,7 @@ const initialState = Immutable.Map({ isLoading: true, }); -const notificationToMap = notification => Immutable.Map({ +const notificationToMap = notification => ImmutableMap({ id: notification.id, type: notification.type, account: notification.account.id, @@ -46,7 +46,7 @@ const normalizeNotification = (state, notification) => { }; const normalizeNotifications = (state, notifications, next) => { - let items = Immutable.List(); + let items = ImmutableList(); const loaded = state.get('loaded'); notifications.forEach((n, i) => { @@ -64,7 +64,7 @@ const normalizeNotifications = (state, notifications, next) => { }; const appendNormalizedNotifications = (state, notifications, next) => { - let items = Immutable.List(); + let items = ImmutableList(); notifications.forEach((n, i) => { items = items.set(i, notificationToMap(n)); @@ -110,7 +110,7 @@ export default function notifications(state = initialState, action) { case ACCOUNT_BLOCK_SUCCESS: return filterNotifications(state, action.relationship); case NOTIFICATIONS_CLEAR: - return state.set('items', Immutable.List()).set('next', null); + return state.set('items', ImmutableList()).set('next', null); case TIMELINE_DELETE: return deleteByStatus(state, action.id); default: diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js index b6607860cca..c7b04a66836 100644 --- a/app/javascript/mastodon/reducers/relationships.js +++ b/app/javascript/mastodon/reducers/relationships.js @@ -11,9 +11,9 @@ import { DOMAIN_BLOCK_SUCCESS, DOMAIN_UNBLOCK_SUCCESS, } from '../actions/domain_blocks'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; -const normalizeRelationship = (state, relationship) => state.set(relationship.id, Immutable.fromJS(relationship)); +const normalizeRelationship = (state, relationship) => state.set(relationship.id, fromJS(relationship)); const normalizeRelationships = (state, relationships) => { relationships.forEach(relationship => { @@ -23,7 +23,7 @@ const normalizeRelationships = (state, relationships) => { return state; }; -const initialState = Immutable.Map(); +const initialState = ImmutableMap(); export default function relationships(state = initialState, action) { switch(action.type) { diff --git a/app/javascript/mastodon/reducers/reports.js b/app/javascript/mastodon/reducers/reports.js index ad35eaa059f..283c5b6f5e3 100644 --- a/app/javascript/mastodon/reducers/reports.js +++ b/app/javascript/mastodon/reducers/reports.js @@ -7,13 +7,13 @@ import { REPORT_STATUS_TOGGLE, REPORT_COMMENT_CHANGE, } from '../actions/reports'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, Set as ImmutableSet } from 'immutable'; -const initialState = Immutable.Map({ - new: Immutable.Map({ +const initialState = ImmutableMap({ + new: ImmutableMap({ isSubmitting: false, account_id: null, - status_ids: Immutable.Set(), + status_ids: ImmutableSet(), comment: '', }), }); @@ -26,14 +26,14 @@ export default function reports(state = initialState, action) { map.setIn(['new', 'account_id'], action.account.get('id')); if (state.getIn(['new', 'account_id']) !== action.account.get('id')) { - map.setIn(['new', 'status_ids'], action.status ? Immutable.Set([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : Immutable.Set()); + map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.getIn(['reblog', 'id'], action.status.get('id'))]) : ImmutableSet()); map.setIn(['new', 'comment'], ''); } else { - map.updateIn(['new', 'status_ids'], Immutable.Set(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id')))); + map.updateIn(['new', 'status_ids'], ImmutableSet(), set => set.add(action.status.getIn(['reblog', 'id'], action.status.get('id')))); } }); case REPORT_STATUS_TOGGLE: - return state.updateIn(['new', 'status_ids'], Immutable.Set(), set => { + return state.updateIn(['new', 'status_ids'], ImmutableSet(), set => { if (action.checked) { return set.add(action.statusId); } @@ -50,7 +50,7 @@ export default function reports(state = initialState, action) { case REPORT_SUBMIT_SUCCESS: return state.withMutations(map => { map.setIn(['new', 'account_id'], null); - map.setIn(['new', 'status_ids'], Immutable.Set()); + map.setIn(['new', 'status_ids'], ImmutableSet()); map.setIn(['new', 'comment'], ''); map.setIn(['new', 'isSubmitting'], false); }); diff --git a/app/javascript/mastodon/reducers/search.js b/app/javascript/mastodon/reducers/search.js index 0a3adac05cc..08d90e4e83c 100644 --- a/app/javascript/mastodon/reducers/search.js +++ b/app/javascript/mastodon/reducers/search.js @@ -5,13 +5,13 @@ import { SEARCH_SHOW, } from '../actions/search'; import { COMPOSE_MENTION, COMPOSE_REPLY } from '../actions/compose'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -const initialState = Immutable.Map({ +const initialState = ImmutableMap({ value: '', submitted: false, hidden: false, - results: Immutable.Map(), + results: ImmutableMap(), }); export default function search(state = initialState, action) { @@ -21,7 +21,7 @@ export default function search(state = initialState, action) { case SEARCH_CLEAR: return state.withMutations(map => { map.set('value', ''); - map.set('results', Immutable.Map()); + map.set('results', ImmutableMap()); map.set('submitted', false); map.set('hidden', false); }); @@ -31,10 +31,10 @@ export default function search(state = initialState, action) { case COMPOSE_MENTION: return state.set('hidden', true); case SEARCH_FETCH_SUCCESS: - return state.set('results', Immutable.Map({ - accounts: Immutable.List(action.results.accounts.map(item => item.id)), - statuses: Immutable.List(action.results.statuses.map(item => item.id)), - hashtags: Immutable.List(action.results.hashtags), + return state.set('results', ImmutableMap({ + accounts: ImmutableList(action.results.accounts.map(item => item.id)), + statuses: ImmutableList(action.results.statuses.map(item => item.id)), + hashtags: ImmutableList(action.results.hashtags), })).set('submitted', true); default: return state; diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index ddad7a4fc2d..dd2d76ec02c 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -1,39 +1,39 @@ import { SETTING_CHANGE } from '../actions/settings'; import { COLUMN_ADD, COLUMN_REMOVE, COLUMN_MOVE } from '../actions/columns'; import { STORE_HYDRATE } from '../actions/store'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; import uuid from '../uuid'; -const initialState = Immutable.Map({ +const initialState = ImmutableMap({ onboarded: false, - home: Immutable.Map({ - shows: Immutable.Map({ + home: ImmutableMap({ + shows: ImmutableMap({ reblog: true, reply: true, }), - regex: Immutable.Map({ + regex: ImmutableMap({ body: '', }), }), - notifications: Immutable.Map({ - alerts: Immutable.Map({ + notifications: ImmutableMap({ + alerts: ImmutableMap({ follow: true, favourite: true, reblog: true, mention: true, }), - shows: Immutable.Map({ + shows: ImmutableMap({ follow: true, favourite: true, reblog: true, mention: true, }), - sounds: Immutable.Map({ + sounds: ImmutableMap({ follow: true, favourite: true, reblog: true, @@ -41,20 +41,20 @@ const initialState = Immutable.Map({ }), }), - community: Immutable.Map({ - regex: Immutable.Map({ + community: ImmutableMap({ + regex: ImmutableMap({ body: '', }), }), - public: Immutable.Map({ - regex: Immutable.Map({ + public: ImmutableMap({ + regex: ImmutableMap({ body: '', }), }), }); -const defaultColumns = Immutable.fromJS([ +const defaultColumns = fromJS([ { id: 'COMPOSE', uuid: uuid(), params: {} }, { id: 'HOME', uuid: uuid(), params: {} }, { id: 'NOTIFICATIONS', uuid: uuid(), params: {} }, @@ -82,7 +82,7 @@ export default function settings(state = initialState, action) { case SETTING_CHANGE: return state.setIn(action.key, action.value); case COLUMN_ADD: - return state.update('columns', list => list.push(Immutable.fromJS({ id: action.id, uuid: uuid(), params: action.params }))); + return state.update('columns', list => list.push(fromJS({ id: action.id, uuid: uuid(), params: action.params }))); case COLUMN_REMOVE: return state.update('columns', list => list.filterNot(item => item.get('uuid') === action.uuid)); case COLUMN_MOVE: diff --git a/app/javascript/mastodon/reducers/status_lists.js b/app/javascript/mastodon/reducers/status_lists.js index 7d00f6d309f..bbc9733027a 100644 --- a/app/javascript/mastodon/reducers/status_lists.js +++ b/app/javascript/mastodon/reducers/status_lists.js @@ -2,13 +2,13 @@ import { FAVOURITED_STATUSES_FETCH_SUCCESS, FAVOURITED_STATUSES_EXPAND_SUCCESS, } from '../actions/favourites'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -const initialState = Immutable.Map({ - favourites: Immutable.Map({ +const initialState = ImmutableMap({ + favourites: ImmutableMap({ next: null, loaded: false, - items: Immutable.List(), + items: ImmutableList(), }), }); @@ -16,7 +16,7 @@ const normalizeList = (state, listType, statuses, next) => { return state.update(listType, listMap => listMap.withMutations(map => { map.set('next', next); map.set('loaded', true); - map.set('items', Immutable.List(statuses.map(item => item.id))); + map.set('items', ImmutableList(statuses.map(item => item.id))); })); }; diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 691135ff7df..b1b1d0988e2 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -33,7 +33,7 @@ import { FAVOURITED_STATUSES_EXPAND_SUCCESS, } from '../actions/favourites'; import { SEARCH_FETCH_SUCCESS } from '../actions/search'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, fromJS } from 'immutable'; const normalizeStatus = (state, status) => { if (!status) { @@ -51,7 +51,7 @@ const normalizeStatus = (state, status) => { const searchContent = [status.spoiler_text, status.content].join(' ').replace(/
/g, '\n').replace(/<\/p>

/g, '\n\n'); normalStatus.search_index = new DOMParser().parseFromString(searchContent, 'text/html').documentElement.textContent; - return state.update(status.id, Immutable.Map(), map => map.mergeDeep(Immutable.fromJS(normalStatus))); + return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus))); }; const normalizeStatuses = (state, statuses) => { @@ -82,7 +82,7 @@ const filterStatuses = (state, relationship) => { return state; }; -const initialState = Immutable.Map(); +const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { switch(action.type) { diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 1b738a16aa1..065e89f9617 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -15,25 +15,25 @@ import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS, } from '../actions/accounts'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; -const initialState = Immutable.Map(); +const initialState = ImmutableMap(); -const initialTimeline = Immutable.Map({ +const initialTimeline = ImmutableMap({ unread: 0, online: false, top: true, loaded: false, isLoading: false, next: false, - items: Immutable.List(), + items: ImmutableList(), }); const normalizeTimeline = (state, timeline, statuses, next) => { - const ids = Immutable.List(statuses.map(status => status.get('id'))); + const ids = ImmutableList(statuses.map(status => status.get('id'))); const wasLoaded = state.getIn([timeline, 'loaded']); const hadNext = state.getIn([timeline, 'next']); - const oldIds = state.getIn([timeline, 'items'], Immutable.List()); + const oldIds = state.getIn([timeline, 'items'], ImmutableList()); return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('loaded', true); @@ -44,8 +44,8 @@ const normalizeTimeline = (state, timeline, statuses, next) => { }; const appendNormalizedTimeline = (state, timeline, statuses, next) => { - const ids = Immutable.List(statuses.map(status => status.get('id'))); - const oldIds = state.getIn([timeline, 'items'], Immutable.List()); + const ids = ImmutableList(statuses.map(status => status.get('id'))); + const oldIds = state.getIn([timeline, 'items'], ImmutableList()); return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { mMap.set('isLoading', false); @@ -56,7 +56,7 @@ const appendNormalizedTimeline = (state, timeline, statuses, next) => { const updateTimeline = (state, timeline, status, references) => { const top = state.getIn([timeline, 'top']); - const ids = state.getIn([timeline, 'items'], Immutable.List()); + const ids = state.getIn([timeline, 'items'], ImmutableList()); const includesId = ids.includes(status.get('id')); const unread = state.getIn([timeline, 'unread'], 0); @@ -124,11 +124,11 @@ export default function timelines(state = initialState, action) { case TIMELINE_EXPAND_FAIL: return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false)); case TIMELINE_REFRESH_SUCCESS: - return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next); + return normalizeTimeline(state, action.timeline, fromJS(action.statuses), action.next); case TIMELINE_EXPAND_SUCCESS: - return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next); + return appendNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next); case TIMELINE_UPDATE: - return updateTimeline(state, action.timeline, Immutable.fromJS(action.status), action.references); + return updateTimeline(state, action.timeline, fromJS(action.status), action.references); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); case ACCOUNT_BLOCK_SUCCESS: diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index 83bf1be1bc6..8db18c5dc66 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -20,22 +20,22 @@ import { MUTES_FETCH_SUCCESS, MUTES_EXPAND_SUCCESS, } from '../actions/mutes'; -import Immutable from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; -const initialState = Immutable.Map({ - followers: Immutable.Map(), - following: Immutable.Map(), - reblogged_by: Immutable.Map(), - favourited_by: Immutable.Map(), - follow_requests: Immutable.Map(), - blocks: Immutable.Map(), - mutes: Immutable.Map(), +const initialState = ImmutableMap({ + followers: ImmutableMap(), + following: ImmutableMap(), + reblogged_by: ImmutableMap(), + favourited_by: ImmutableMap(), + follow_requests: ImmutableMap(), + blocks: ImmutableMap(), + mutes: ImmutableMap(), }); const normalizeList = (state, type, id, accounts, next) => { - return state.setIn([type, id], Immutable.Map({ + return state.setIn([type, id], ImmutableMap({ next, - items: Immutable.List(accounts.map(item => item.id)), + items: ImmutableList(accounts.map(item => item.id)), })); }; @@ -56,22 +56,22 @@ export default function userLists(state = initialState, action) { case FOLLOWING_EXPAND_SUCCESS: return appendToList(state, 'following', action.id, action.accounts, action.next); case REBLOGS_FETCH_SUCCESS: - return state.setIn(['reblogged_by', action.id], Immutable.List(action.accounts.map(item => item.id))); + return state.setIn(['reblogged_by', action.id], ImmutableList(action.accounts.map(item => item.id))); case FAVOURITES_FETCH_SUCCESS: - return state.setIn(['favourited_by', action.id], Immutable.List(action.accounts.map(item => item.id))); + return state.setIn(['favourited_by', action.id], ImmutableList(action.accounts.map(item => item.id))); case FOLLOW_REQUESTS_FETCH_SUCCESS: - return state.setIn(['follow_requests', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next); + return state.setIn(['follow_requests', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next); case FOLLOW_REQUESTS_EXPAND_SUCCESS: return state.updateIn(['follow_requests', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['follow_requests', 'next'], action.next); case FOLLOW_REQUEST_AUTHORIZE_SUCCESS: case FOLLOW_REQUEST_REJECT_SUCCESS: return state.updateIn(['follow_requests', 'items'], list => list.filterNot(item => item === action.id)); case BLOCKS_FETCH_SUCCESS: - return state.setIn(['blocks', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); + return state.setIn(['blocks', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); case BLOCKS_EXPAND_SUCCESS: return state.updateIn(['blocks', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['blocks', 'next'], action.next); case MUTES_FETCH_SUCCESS: - return state.setIn(['mutes', 'items'], Immutable.List(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); + return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); case MUTES_EXPAND_SUCCESS: return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next); default: diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 07d9a2629bc..d26d1b727f6 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import Immutable from 'immutable'; +import { List as ImmutableList } from 'immutable'; const getAccountBase = (state, id) => state.getIn(['accounts', id], null); const getAccountCounters = (state, id) => state.getIn(['accounts_counters', id], null); @@ -73,10 +73,10 @@ export const makeGetNotification = () => { }; export const getAccountGallery = createSelector([ - (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], Immutable.List()), + (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], ImmutableList()), state => state.get('statuses'), ], (statusIds, statuses) => { - let medias = Immutable.List(); + let medias = ImmutableList(); statusIds.forEach(statusId => { const status = statuses.get(statusId); diff --git a/spec/javascript/components/display_name.test.js b/spec/javascript/components/display_name.test.js index d6dc7edc091..ad9288d4dfc 100644 --- a/spec/javascript/components/display_name.test.js +++ b/spec/javascript/components/display_name.test.js @@ -1,12 +1,12 @@ import { expect } from 'chai'; import { render } from 'enzyme'; -import Immutable from 'immutable'; +import { fromJS } from 'immutable'; import React from 'react'; import DisplayName from '../../../app/javascript/mastodon/components/display_name'; describe('', () => { it('renders display name + account name', () => { - const account = Immutable.fromJS({ + const account = fromJS({ username: 'bar', acct: 'bar@baz', display_name: 'Foo', @@ -16,7 +16,7 @@ describe('', () => { }); it('renders the username + account name if display name is empty', () => { - const account = Immutable.fromJS({ + const account = fromJS({ username: 'bar', acct: 'bar@baz', display_name: '', From 29f314a50209f64b551be7fae2744234bcca2627 Mon Sep 17 00:00:00 2001 From: "Akihiko Odaki (@fn_aki@pawoo.net)" Date: Tue, 11 Jul 2017 18:55:48 +0900 Subject: [PATCH 104/114] Remove redundant inclusion (#4150) --- app/services/process_feed_service.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb index 028962d5be0..c335d21596c 100644 --- a/app/services/process_feed_service.rb +++ b/app/services/process_feed_service.rb @@ -20,8 +20,6 @@ class ProcessFeedService < BaseService end class ProcessEntry - include AuthorExtractor - def call(xml, account) @account = account @xml = xml From 425acecfdb15093a265b191120fb2d4e4c4135c4 Mon Sep 17 00:00:00 2001 From: "Akihiko Odaki (@fn_aki@pawoo.net)" Date: Tue, 11 Jul 2017 20:37:05 +0900 Subject: [PATCH 105/114] Wrap methods of ProcessFeedService::ProcessEntry in classes (#4151) ProcessFeedService::ProcessEntry had many methods, so wrap them in classes representing activities. --- app/services/process_feed_service.rb | 433 ++++++++++++++------------- 1 file changed, 225 insertions(+), 208 deletions(-) diff --git a/app/services/process_feed_service.rb b/app/services/process_feed_service.rb index c335d21596c..19ab0c858b8 100644 --- a/app/services/process_feed_service.rb +++ b/app/services/process_feed_service.rb @@ -22,268 +22,285 @@ class ProcessFeedService < BaseService class ProcessEntry def call(xml, account) @account = account - @xml = xml + @fetched = Activity.new(xml) - return if skip_unsupported_type? + return unless [:activity, :note, :comment].include?(@fetched.type) - case verb - when :post, :share - return create_status - when :delete - return delete_status - end + klass = case @fetched.verb + when :post + PostActivity + when :share + ShareActivity + when :delete + DeletionActivity + else + return + end + + @fetched = klass.new(xml, account) + @fetched.perform rescue ActiveRecord::RecordInvalid => e Rails.logger.debug "Nothing was saved for #{id} because: #{e}" nil end - private - - def create_status - if redis.exists("delete_upon_arrival:#{@account.id}:#{id}") - Rails.logger.debug "Delete for status #{id} was queued, ignoring" - return + class Activity + def initialize(xml, account = nil) + @xml = xml + @account = account end - status, just_created = nil - - Rails.logger.debug "Creating remote status #{id}" - - if verb == :share - original_status = shared_status_from_xml(@xml.at_xpath('.//activity:object', activity: TagManager::AS_XMLNS)) - return nil if original_status.nil? + def verb + raw = @xml.at_xpath('./activity:verb', activity: TagManager::AS_XMLNS).content + TagManager::VERBS.key(raw) + rescue + :post end - ApplicationRecord.transaction do - status, just_created = status_from_xml(@xml) + def type + raw = @xml.at_xpath('./activity:object-type', activity: TagManager::AS_XMLNS).content + TagManager::TYPES.key(raw) + rescue + :activity + end - return if status.nil? - return status unless just_created + def id + @xml.at_xpath('./xmlns:id', xmlns: TagManager::XMLNS).content + end - if verb == :share - status.reblog = original_status.reblog? ? original_status.reblog : original_status + def url + link = @xml.at_xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS) + link.nil? ? nil : link['href'] + end + + private + + def find_status(uri) + if TagManager.instance.local_id?(uri) + local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Status') + return Status.find_by(id: local_id) end - status.save! + Status.find_by(uri: uri) end - if thread?(@xml) && status.thread.nil? - Rails.logger.debug "Trying to attach #{status.id} (#{id(@xml)}) to #{thread(@xml).first}" - ThreadResolveWorker.perform_async(status.id, thread(@xml).second) - end - - notify_about_mentions!(status) unless status.reblog? - notify_about_reblog!(status) if status.reblog? && status.reblog.account.local? - - Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution" - - LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? - DistributionWorker.perform_async(status.id) - - status - end - - def notify_about_mentions!(status) - status.mentions.includes(:account).each do |mention| - mentioned_account = mention.account - next unless mentioned_account.local? - NotifyService.new.call(mentioned_account, mention) + def redis + Redis.current end end - def notify_about_reblog!(status) - NotifyService.new.call(status.reblog.account, status) - end + class CreationActivity < Activity + def perform + if redis.exists("delete_upon_arrival:#{@account.id}:#{id}") + Rails.logger.debug "Delete for status #{id} was queued, ignoring" + return + end - def delete_status - Rails.logger.debug "Deleting remote status #{id}" - status = Status.find_by(uri: id, account: @account) + Rails.logger.debug "Creating remote status #{id}" + # Return early if status already exists in db + status = find_status(id) - if status.nil? - redis.setex("delete_upon_arrival:#{@account.id}:#{id}", 6 * 3_600, id) - else - RemoveStatusService.new.call(status) - end - end + return [status, false] unless status.nil? - def skip_unsupported_type? - !([:post, :share, :delete].include?(verb) && [:activity, :note, :comment].include?(type)) - end + return [nil, false] if @account.suspended? - def shared_status_from_xml(entry) - status = find_status(id(entry)) + status = Status.create!( + uri: id, + url: url, + account: @account, + reblog: reblog, + text: content, + spoiler_text: content_warning, + created_at: published, + reply: thread?, + language: content_language, + visibility: visibility_scope, + conversation: converstation_to_persistent&.first, + thread: thread? ? find_status(thread.first) : nil + ) - return status unless status.nil? + save_mentions(status) + save_hashtags(status) + save_media(status) - FetchRemoteStatusService.new.call(url(entry)) - end + if thread? && status.thread.nil? + Rails.logger.debug "Trying to attach #{status.id} (#{id}) to #{thread.first}" + ThreadResolveWorker.perform_async(status.id, thread.second) + end - def status_from_xml(entry) - # Return early if status already exists in db - status = find_status(id(entry)) + Rails.logger.debug "Queuing remote status #{status.id} (#{id}) for distribution" - return [status, false] unless status.nil? + LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text? + DistributionWorker.perform_async(status.id) - account = @account - - return [nil, false] if account.suspended? - - status = Status.create!( - uri: id(entry), - url: url(entry), - account: account, - text: content(entry), - spoiler_text: content_warning(entry), - created_at: published(entry), - reply: thread?(entry), - language: content_language(entry), - visibility: visibility_scope(entry), - conversation: find_or_create_conversation(entry), - thread: thread?(entry) ? find_status(thread(entry).first) : nil - ) - - mentions_from_xml(status, entry) - hashtags_from_xml(status, entry) - media_from_xml(status, entry) - - [status, true] - end - - def find_or_create_conversation(xml) - uri = xml.at_xpath('./ostatus:conversation', ostatus: TagManager::OS_XMLNS)&.attribute('ref')&.content - return if uri.nil? - - if TagManager.instance.local_id?(uri) - local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Conversation') - return Conversation.find_by(id: local_id) + [status, true] end - Conversation.find_by(uri: uri) || Conversation.create!(uri: uri) - end - - def find_status(uri) - if TagManager.instance.local_id?(uri) - local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Status') - return Status.find_by(id: local_id) + def content + @xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS).content end - Status.find_by(uri: uri) - end - - def mentions_from_xml(parent, xml) - processed_account_ids = [] - - xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each do |link| - next if [TagManager::TYPES[:group], TagManager::TYPES[:collection]].include? link['ostatus:object-type'] - - mentioned_account = account_from_href(link['href']) - - next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id) - - mentioned_account.mentions.where(status: parent).first_or_create(status: parent) - - # So we can skip duplicate mentions - processed_account_ids << mentioned_account.id + def content_language + @xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS)['xml:lang']&.presence || 'en' end - end - def account_from_href(href) - url = Addressable::URI.parse(href).normalize - - if TagManager.instance.web_domain?(url.host) - Account.find_local(url.path.gsub('/users/', '')) - else - Account.where(uri: href).or(Account.where(url: href)).first || FetchRemoteAccountService.new.call(href) + def content_warning + @xml.at_xpath('./xmlns:summary', xmlns: TagManager::XMLNS)&.content || '' end - end - def hashtags_from_xml(parent, xml) - tags = xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select(&:present?) - ProcessHashtagsService.new.call(parent, tags) - end + def visibility_scope + @xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content&.to_sym || :public + end - def media_from_xml(parent, xml) - do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? + def published + @xml.at_xpath('./xmlns:published', xmlns: TagManager::XMLNS).content + end - xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: TagManager::XMLNS).each do |link| - next unless link['href'] + def thread? + !@xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS).nil? + end - media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href']) - parsed_url = Addressable::URI.parse(link['href']).normalize + def thread + thr = @xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS) + [thr['ref'], thr['href']] + end - next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty? + private - media.save + def converstation_to_persistent + uri = @xml.at_xpath('./ostatus:conversation', ostatus: TagManager::OS_XMLNS)&.attribute('ref')&.content + return if uri.nil? - next if do_not_download + if TagManager.instance.local_id?(uri) + local_id = TagManager.instance.unique_tag_to_local_id(uri, 'Conversation') + return Conversation.find_by(id: local_id) + end - begin - media.file_remote_url = link['href'] - media.save! - rescue ActiveRecord::RecordInvalid - next + found = Conversation.find_by(uri: uri) + found ? [found, false] : [Conversation.create!(uri: uri), true] + end + + def save_mentions(parent) + processed_account_ids = [] + + @xml.xpath('./xmlns:link[@rel="mentioned"]', xmlns: TagManager::XMLNS).each do |link| + next if [TagManager::TYPES[:group], TagManager::TYPES[:collection]].include? link['ostatus:object-type'] + + mentioned_account = account_from_href(link['href']) + + next if mentioned_account.nil? || processed_account_ids.include?(mentioned_account.id) + + mentioned_account.mentions.where(status: parent).first_or_create(status: parent) + + # So we can skip duplicate mentions + processed_account_ids << mentioned_account.id + end + end + + def save_hashtags(parent) + tags = @xml.xpath('./xmlns:category', xmlns: TagManager::XMLNS).map { |category| category['term'] }.select(&:present?) + ProcessHashtagsService.new.call(parent, tags) + end + + def save_media(parent) + do_not_download = DomainBlock.find_by(domain: parent.account.domain)&.reject_media? + + @xml.xpath('./xmlns:link[@rel="enclosure"]', xmlns: TagManager::XMLNS).each do |link| + next unless link['href'] + + media = MediaAttachment.where(status: parent, remote_url: link['href']).first_or_initialize(account: parent.account, status: parent, remote_url: link['href']) + parsed_url = Addressable::URI.parse(link['href']).normalize + + next if !%w(http https).include?(parsed_url.scheme) || parsed_url.host.empty? + + media.save + + next if do_not_download + + begin + media.file_remote_url = link['href'] + media.save! + rescue ActiveRecord::RecordInvalid + next + end + end + end + + def account_from_href(href) + url = Addressable::URI.parse(href).normalize + + if TagManager.instance.web_domain?(url.host) + Account.find_local(url.path.gsub('/users/', '')) + else + Account.where(uri: href).or(Account.where(url: href)).first || FetchRemoteAccountService.new.call(href) end end end - def id(xml = @xml) - xml.at_xpath('./xmlns:id', xmlns: TagManager::XMLNS).content + class ShareActivity < CreationActivity + def perform + status, just_created = super + NotifyService.new.call(reblog.account, status) if reblog&.account&.local? && just_created + status + end + + def object + @xml.at_xpath('.//activity:object', activity: TagManager::AS_XMLNS) + end + + private + + def status + reblog && super + end + + def reblog + return @reblog if defined? @reblog + + original_status = RemoteActivity.new(object).perform + @reblog = original_status.reblog? ? original_status.reblog : original_status + end end - def verb(xml = @xml) - raw = xml.at_xpath('./activity:verb', activity: TagManager::AS_XMLNS).content - TagManager::VERBS.key(raw) - rescue - :post + class PostActivity < CreationActivity + def perform + status, just_created = super + + if just_created + status.mentions.includes(:account).each do |mention| + mentioned_account = mention.account + next unless mentioned_account.local? + NotifyService.new.call(mentioned_account, mention) + end + end + + status + end + + private + + def reblog + nil + end end - def type(xml = @xml) - raw = xml.at_xpath('./activity:object-type', activity: TagManager::AS_XMLNS).content - TagManager::TYPES.key(raw) - rescue - :activity + class DeletionActivity < Activity + def perform + Rails.logger.debug "Deleting remote status #{id}" + status = Status.find_by(uri: id, account: @account) + + if status.nil? + redis.setex("delete_upon_arrival:#{@account.id}:#{id}", 6 * 3_600, id) + else + RemoveStatusService.new.call(status) + end + end end - def url(xml = @xml) - link = xml.at_xpath('./xmlns:link[@rel="alternate"]', xmlns: TagManager::XMLNS) - link.nil? ? nil : link['href'] - end - - def content(xml = @xml) - xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS).content - end - - def content_language(xml = @xml) - xml.at_xpath('./xmlns:content', xmlns: TagManager::XMLNS)['xml:lang']&.presence || 'en' - end - - def content_warning(xml = @xml) - xml.at_xpath('./xmlns:summary', xmlns: TagManager::XMLNS)&.content || '' - end - - def visibility_scope(xml = @xml) - xml.at_xpath('./mastodon:scope', mastodon: TagManager::MTDN_XMLNS)&.content&.to_sym || :public - end - - def published(xml = @xml) - xml.at_xpath('./xmlns:published', xmlns: TagManager::XMLNS).content - end - - def thread?(xml = @xml) - !xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS).nil? - end - - def thread(xml = @xml) - thr = xml.at_xpath('./thr:in-reply-to', thr: TagManager::THR_XMLNS) - [thr['ref'], thr['href']] - end - - def account?(xml = @xml) - !xml.at_xpath('./xmlns:author', xmlns: TagManager::XMLNS).nil? - end - - def redis - Redis.current + class RemoteActivity < Activity + def perform + find_status(id) || FetchRemoteStatusService.new.call(url) + end end end end From 31366334cb186974fe97234e05fe36d19d33841b Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Tue, 11 Jul 2017 21:36:27 +0900 Subject: [PATCH 106/114] Drawer tab according to column (#4135) * Add notifications link to drawer * Remove local and public timeline tab in drawer * Add home --- .../mastodon/features/compose/index.js | 20 +++++++++++++++++-- .../mastodon/locales/defaultMessages.json | 8 ++++++++ app/javascript/styles/components.scss | 3 +-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js index 747fe421641..6aa5de96c05 100644 --- a/app/javascript/mastodon/features/compose/index.js +++ b/app/javascript/mastodon/features/compose/index.js @@ -2,6 +2,7 @@ import React from 'react'; import ComposeFormContainer from './containers/compose_form_container'; import NavigationContainer from './containers/navigation_container'; import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { mountCompose, unmountCompose } from '../../actions/compose'; import Link from 'react-router-dom/Link'; @@ -13,6 +14,8 @@ import SearchResultsContainer from './containers/search_results_container'; const messages = defineMessages({ 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' }, community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, @@ -20,6 +23,7 @@ const messages = defineMessages({ }); const mapStateToProps = state => ({ + columns: state.getIn(['settings', 'columns']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), }); @@ -29,6 +33,7 @@ export default class Compose extends React.PureComponent { static propTypes = { dispatch: PropTypes.func.isRequired, + columns: ImmutablePropTypes.list.isRequired, multiColumn: PropTypes.bool, showSearch: PropTypes.bool, intl: PropTypes.object.isRequired, @@ -48,11 +53,22 @@ export default class Compose extends React.PureComponent { let header = ''; if (multiColumn) { + const { columns } = this.props; header = (

diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 88f0f9c30a6..7c1522299ac 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -635,6 +635,14 @@ "defaultMessage": "Getting started", "id": "getting_started.heading" }, + { + "defaultMessage": "Home", + "id": "tabs_bar.home" + }, + { + "defaultMessage": "Notifications", + "id": "tabs_bar.notifications" + }, { "defaultMessage": "Federated timeline", "id": "navigation_bar.public_timeline" diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 97651b5f46d..def69d250dc 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1317,8 +1317,7 @@ .drawer__tab { display: block; flex: 1 1 auto; - padding: 15px; - padding-bottom: 13px; + padding: 15px 5px 13px; color: $ui-primary-color; text-decoration: none; text-align: center; From 8784bd79d0053cb15775eb078f45e6aca7775d77 Mon Sep 17 00:00:00 2001 From: "Akihiko Odaki (@fn_aki@pawoo.net)" Date: Tue, 11 Jul 2017 22:15:42 +0900 Subject: [PATCH 107/114] Require stylesheets in common.js (#4152) Require stylesheets in common.js because stylesheets are shared by the entry points. --- app/javascript/mastodon/main.js | 4 ---- app/javascript/packs/common.js | 5 +++++ app/views/layouts/application.html.haml | 2 +- app/views/layouts/embedded.html.haml | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js index aca64c07512..d7ffa8ea6bf 100644 --- a/app/javascript/mastodon/main.js +++ b/app/javascript/mastodon/main.js @@ -1,9 +1,5 @@ const perf = require('./performance'); -// import default stylesheet with variables -require('font-awesome/css/font-awesome.css'); -require('mastodon-application-style'); - function onDomContentLoaded(callback) { if (document.readyState !== 'loading') { callback(); diff --git a/app/javascript/packs/common.js b/app/javascript/packs/common.js index 9d63d8f982e..a0cb91ae4b9 100644 --- a/app/javascript/packs/common.js +++ b/app/javascript/packs/common.js @@ -1,2 +1,7 @@ import { start } from 'rails-ujs'; + +// import default stylesheet with variables +require('font-awesome/css/font-awesome.css'); +require('mastodon-application-style'); + start(); diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 580d8fb4d29..ef97fb12762 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -18,7 +18,7 @@ = ' - ' = title - = stylesheet_pack_tag 'application', media: 'all' + = stylesheet_pack_tag 'common', media: 'all' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' = javascript_pack_tag 'features/getting_started', integrity: true, crossorigin: 'anonymous', rel: 'preload', as: 'script' diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml index 5680c1ff92a..4826f32f75f 100644 --- a/app/views/layouts/embedded.html.haml +++ b/app/views/layouts/embedded.html.haml @@ -2,7 +2,7 @@ %html{ lang: I18n.locale } %head %meta{ charset: 'utf-8' }/ - = stylesheet_pack_tag 'application', media: 'all' + = stylesheet_pack_tag 'common', media: 'all' = javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous' = javascript_pack_tag "locale_#{I18n.locale}", integrity: true, crossorigin: 'anonymous' = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' From e19eefe219c46ea9f763d0279029f03c5cf4554f Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Tue, 11 Jul 2017 15:27:59 +0200 Subject: [PATCH 108/114] Redesign the landing page, mount public timeline on it (#4122) * Redesign the landing page, mount public timeline on it * Adjust the standalone mounted component to the lacking of router * Adjust auth layout pages to new design * Fix tests * Standalone public timeline polling every 5 seconds * Remove now obsolete translations * Add responsive design for new landing page * Address reviews * Add floating clouds behind frontpage form * Use access token from public page when available * Fix mentions and hashtags links, cursor on status content in standalone mode * Add footer link to source code * Fix errors on pages that don't embed the component, use classnames * Fix tests * Change anonymous autoPlayGif default to false * When gif autoplay is disabled, hover to play * Add option to hide the timeline preview * Slightly improve alt layout * Add elephant friend to new frontpage * Display "back to mastodon" in place of "login" when logged in on frontpage * Change polling time to 3s --- app/controllers/about_controller.rb | 13 +- app/controllers/admin/settings_controller.rb | 9 +- app/controllers/home_controller.rb | 16 +- .../fonts/montserrat/Montserrat-Medium.ttf | Bin 0 -> 192488 bytes app/javascript/images/cloud2.png | Bin 0 -> 4973 bytes app/javascript/images/cloud3.png | Bin 0 -> 5860 bytes app/javascript/images/cloud4.png | Bin 0 -> 5273 bytes app/javascript/images/elephant-fren.png | Bin 0 -> 40859 bytes app/javascript/images/logo.svg | 2 +- .../mastodon/components/dropdown_menu.js | 19 +- .../mastodon/components/media_gallery.js | 38 +- .../mastodon/components/permalink.js | 4 +- app/javascript/mastodon/components/status.js | 8 +- .../mastodon/components/status_action_bar.js | 11 +- .../mastodon/components/status_content.js | 17 +- .../mastodon/components/video_player.js | 22 +- .../mastodon/containers/timeline_container.js | 39 ++ .../standalone/public_timeline/index.js | 76 +++ app/javascript/packs/public.js | 10 + app/javascript/styles/about.scss | 454 ++++++++++++++++-- app/javascript/styles/basics.scss | 7 +- app/javascript/styles/boost.scss | 4 + app/javascript/styles/components.scss | 32 +- app/javascript/styles/containers.scss | 48 +- app/javascript/styles/fonts/montserrat.scss | 8 + app/javascript/styles/forms.scss | 39 +- app/presenters/instance_presenter.rb | 1 + app/serializers/initial_state_serializer.rb | 35 +- app/views/about/_features.html.haml | 25 + app/views/about/_registration.html.haml | 20 +- app/views/about/show.html.haml | 120 +++-- app/views/admin/settings/edit.html.haml | 43 +- app/views/auth/registrations/new.html.haml | 6 +- app/views/layouts/auth.html.haml | 3 +- config/locales/ar.yml | 13 +- config/locales/bg.yml | 13 +- config/locales/ca.yml | 17 +- config/locales/de.yml | 13 - config/locales/en.yml | 43 +- config/locales/eo.yml | 23 +- config/locales/es.yml | 13 +- config/locales/fa.yml | 13 - config/locales/fi.yml | 13 +- config/locales/fr.yml | 15 +- config/locales/he.yml | 13 - config/locales/hr.yml | 13 +- config/locales/id.yml | 13 - config/locales/io.yml | 13 - config/locales/it.yml | 13 +- config/locales/ja.yml | 15 +- config/locales/ko.yml | 19 +- config/locales/nl.yml | 21 +- config/locales/no.yml | 13 - config/locales/oc.yml | 17 +- config/locales/pl.yml | 19 +- config/locales/pt-BR.yml | 13 - config/locales/pt.yml | 13 - config/locales/ru.yml | 13 - config/locales/th.yml | 13 - config/locales/tr.yml | 13 - config/locales/uk.yml | 13 - config/locales/zh-CN.yml | 13 - config/locales/zh-HK.yml | 13 - config/locales/zh-TW.yml | 13 - config/settings.yml | 1 + lib/tasks/mastodon.rake | 8 +- spec/requests/localization_spec.rb | 8 +- spec/views/about/show.html.haml_spec.rb | 9 +- 68 files changed, 959 insertions(+), 658 deletions(-) create mode 100644 app/javascript/fonts/montserrat/Montserrat-Medium.ttf create mode 100644 app/javascript/images/cloud2.png create mode 100644 app/javascript/images/cloud3.png create mode 100644 app/javascript/images/cloud4.png create mode 100644 app/javascript/images/elephant-fren.png create mode 100644 app/javascript/mastodon/containers/timeline_container.js create mode 100644 app/javascript/mastodon/features/standalone/public_timeline/index.js create mode 100644 app/views/about/_features.html.haml diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index c0addbeccc6..47690e81eb9 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -4,7 +4,10 @@ class AboutController < ApplicationController before_action :set_body_classes 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 @@ -15,6 +18,7 @@ class AboutController < ApplicationController def new_user User.new.tap(&:build_account) end + helper_method :new_user def set_instance_presenter @@ -24,4 +28,11 @@ class AboutController < ApplicationController def set_body_classes @body_classes = 'about-body' end + + def initial_state_params + { + settings: {}, + token: current_session&.token, + } + end end diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index f27a1f4d450..29b590d7a3d 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -11,8 +11,15 @@ module Admin site_terms open_registrations closed_registrations_message + open_deletion + timeline_preview + ).freeze + + BOOLEAN_SETTINGS = %w( + open_registrations + open_deletion + timeline_preview ).freeze - BOOLEAN_SETTINGS = %w(open_registrations).freeze def edit @settings = Setting.all_as_records diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 218da69066f..8a8b9ec76ba 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -15,12 +15,16 @@ class HomeController < ApplicationController end def set_initial_state_json - state = InitialStatePresenter.new(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)) - - serializable_resource = ActiveModelSerializers::SerializableResource.new(state, serializer: InitialStateSerializer) + 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 diff --git a/app/javascript/fonts/montserrat/Montserrat-Medium.ttf b/app/javascript/fonts/montserrat/Montserrat-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..88d70b89c3f2740e9b9c80881616136b5f697091 GIT binary patch literal 192488 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
VsBJm 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 literal 0 HcmV?d00001 diff --git a/app/javascript/images/cloud2.png b/app/javascript/images/cloud2.png new file mode 100644 index 0000000000000000000000000000000000000000..f325ca6de0967bec0b77f532cf7e961adb89b99a GIT binary patch literal 4973 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 b)&0+1YW~Zts=5n4jG^Eo#}Ahu^t=3j6x<=* literal 0 HcmV?d00001 diff --git a/app/javascript/images/cloud3.png b/app/javascript/images/cloud3.png new file mode 100644 index 0000000000000000000000000000000000000000..ab194d0b857a946094418fdf4bd87caadb8d8122 GIT binary patch literal 5860 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 \ No newline at end of file + diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 12e1b44fa22..98323b0691d 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -14,6 +14,7 @@ export default class DropdownMenu extends React.PureComponent { size: PropTypes.number.isRequired, direction: PropTypes.string, ariaLabel: PropTypes.string, + disabled: PropTypes.bool, }; static defaultProps = { @@ -68,9 +69,19 @@ export default class DropdownMenu extends React.PureComponent { } render () { - const { icon, items, size, direction, ariaLabel } = this.props; - const { expanded } = this.state; + const { icon, items, size, direction, ariaLabel, disabled } = this.props; + const { expanded } = this.state; 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 ( +
+ +
+ ); + } const dropdownItems = expanded && (