about summary refs log tree commit diff
path: root/config
diff options
context:
space:
mode:
Diffstat (limited to 'config')
-rw-r--r--config/environments/production.rb13
-rw-r--r--config/i18n-tasks.yml1
-rw-r--r--config/initializers/0_duplicate_migrations.rb52
-rw-r--r--config/initializers/content_security_policy.rb72
-rw-r--r--config/initializers/cors.rb4
-rw-r--r--config/initializers/locale.rb7
-rw-r--r--config/locales-glitch/en.yml23
-rw-r--r--config/locales-glitch/es.yml23
-rw-r--r--config/locales-glitch/ja.yml23
-rw-r--r--config/locales-glitch/ko.yml23
-rw-r--r--config/locales-glitch/pl.yml6
-rw-r--r--config/locales-glitch/simple_form.en.yml20
-rw-r--r--config/locales-glitch/simple_form.es.yml20
-rw-r--r--config/locales-glitch/simple_form.ja.yml20
-rw-r--r--config/locales-glitch/simple_form.ko.yml20
-rw-r--r--config/locales-glitch/simple_form.pl.yml10
-rw-r--r--config/locales-glitch/simple_form.zh-CN.yml20
-rw-r--r--config/locales-glitch/zh-CN.yml23
-rw-r--r--config/navigation.rb6
-rw-r--r--config/routes.rb6
-rw-r--r--config/settings.yml16
-rw-r--r--config/themes.yml3
-rw-r--r--config/webpack/configuration.js57
-rw-r--r--config/webpack/generateLocalePacks.js94
-rw-r--r--config/webpack/rules/babel.js1
-rw-r--r--config/webpack/rules/css.js3
-rw-r--r--config/webpack/shared.js75
-rw-r--r--config/webpacker.yml1
28 files changed, 524 insertions, 118 deletions
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 4a8899944..417d53a8f 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -115,11 +115,14 @@ Rails.application.configure do
   config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym
 
   config.action_dispatch.default_headers = {
-    'Server'                 => 'Mastodon',
-    'X-Frame-Options'        => 'DENY',
-    'X-Content-Type-Options' => 'nosniff',
-    'X-XSS-Protection'       => '1; mode=block',
-    'Permissions-Policy'     => 'interest-cohort=()',
+    'Server'                  => 'Mastodon',
+    'X-Frame-Options'         => 'DENY',
+    'X-Content-Type-Options'  => 'nosniff',
+    'X-XSS-Protection'        => '1; mode=block',
+    'Permissions-Policy'      => 'interest-cohort=()',
+    'Referrer-Policy'         => 'same-origin',
+    'Strict-Transport-Security' => 'max-age=63072000; includeSubDomains; preload',
+    'X-Clacks-Overhead' => 'GNU Natalie Nguyen'
   }
 
   config.x.otp_secret = ENV.fetch('OTP_SECRET')
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 2dc6f880b..e09f4262b 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -59,6 +59,7 @@ ignore_unused:
   - 'errors.429'
   - 'admin.accounts.roles.*'
   - 'admin.action_logs.actions.*'
+  - 'themes.*'
   - 'statuses.attached.*'
   - 'move_handler.carry_{mutes,blocks}_over_text'
 
diff --git a/config/initializers/0_duplicate_migrations.rb b/config/initializers/0_duplicate_migrations.rb
new file mode 100644
index 000000000..6c45e4bd2
--- /dev/null
+++ b/config/initializers/0_duplicate_migrations.rb
@@ -0,0 +1,52 @@
+# Some migrations have been present in glitch-soc for a long time and have then
+# been merged in upstream Mastodon, under a different version number.
+#
+# This puts us in an uneasy situation in which if we remove upstream's
+# migration file, people migrating from upstream will end up having a conflict
+# with their already-ran migration.
+#
+# On the other hand, if we keep upstream's migration and remove our own,
+# any current glitch-soc user will have a conflict during migration.
+#
+# For lack of a better solution, as those migrations are indeed identical,
+# we decided monkey-patching Rails' Migrator to completely ignore the duplicate,
+# keeping only the one that has run, or an arbitrary one.
+
+ALLOWED_DUPLICATES = [20180410220657, 20180831171112].freeze
+
+module ActiveRecord
+  class Migrator
+    def self.new(direction, migrations, schema_migration, target_version = nil)
+      migrated = Set.new(Base.connection.migration_context.get_all_versions)
+
+      migrations.group_by(&:name).each do |name, duplicates|
+        if duplicates.length > 1 && duplicates.all? { |m| ALLOWED_DUPLICATES.include?(m.version) }
+          # We have a set of allowed duplicates. Keep the migrated one, if any.
+          non_migrated = duplicates.reject { |m| migrated.include?(m.version.to_i) }
+
+          if duplicates.length == non_migrated.length || non_migrated.length == 0
+            # There weren't any migrated one, so we have to pick one “canonical” migration
+            migrations = migrations - duplicates[1..-1]
+          else
+            # Just reject every duplicate which hasn't been migrated yet
+            migrations = migrations - non_migrated
+          end
+        end
+      end
+
+      super(direction, migrations, schema_migration, target_version)
+    end
+  end
+
+  class MigrationContext
+    def needs_migration?
+      # A set of duplicated migrations is considered migrated if at least one of
+      # them is migrated.
+      migrated = get_all_versions
+      migrations.group_by(&:name).each do |name, duplicates|
+        return true unless duplicates.any? { |m| migrated.include?(m.version.to_i) }
+      end
+      return false
+    end
+  end
+end
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index b377b7b4d..549ac3568 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -2,43 +2,45 @@
 # For further information see the following documentation
 # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
 
-def host_to_url(str)
-  "http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}" unless str.blank?
-end
+if Rails.env.production?
+  assets_host = Rails.configuration.action_controller.asset_host || "https://#{ENV['WEB_DOMAIN'] || ENV['LOCAL_DOMAIN']}"
+  data_hosts = [assets_host]
 
-base_host = Rails.configuration.x.web_domain
-
-assets_host   = Rails.configuration.action_controller.asset_host
-assets_host ||= host_to_url(base_host)
-
-media_host   = host_to_url(ENV['S3_ALIAS_HOST'])
-media_host ||= host_to_url(ENV['S3_CLOUDFRONT_HOST'])
-media_host ||= host_to_url(ENV['S3_HOSTNAME']) if ENV['S3_ENABLED'] == 'true'
-media_host ||= assets_host
-
-Rails.application.config.content_security_policy do |p|
-  p.base_uri        :none
-  p.default_src     :none
-  p.frame_ancestors :none
-  p.font_src        :self, assets_host
-  p.img_src         :self, :https, :data, :blob, assets_host
-  p.style_src       :self, assets_host
-  p.media_src       :self, :https, :data, assets_host
-  p.frame_src       :self, :https
-  p.manifest_src    :self, assets_host
-
-  if Rails.env.development?
-    webpacker_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{Webpacker.dev_server.host_with_port}" }
-
-    p.connect_src :self, :data, :blob, assets_host, media_host, Rails.configuration.x.streaming_api_base_url, *webpacker_urls
-    p.script_src  :self, :unsafe_inline, :unsafe_eval, assets_host
-    p.child_src   :self, :blob, assets_host
-    p.worker_src  :self, :blob, assets_host
+  if ENV['S3_ENABLED'] == 'true'
+    attachments_host = "https://#{ENV['S3_ALIAS_HOST'] || ENV['S3_CLOUDFRONT_HOST'] || ENV['S3_HOSTNAME'] || "s3-#{ENV['S3_REGION'] || 'us-east-1'}.amazonaws.com"}"
+    attachments_host = "https://#{Addressable::URI.parse(attachments_host).host}"
+  elsif ENV['SWIFT_ENABLED'] == 'true'
+    attachments_host = ENV['SWIFT_OBJECT_URL']
+    attachments_host = "https://#{Addressable::URI.parse(attachments_host).host}"
   else
-    p.connect_src :self, :data, :blob, assets_host, media_host, Rails.configuration.x.streaming_api_base_url
-    p.script_src  :self, assets_host
-    p.child_src   :self, :blob, assets_host
-    p.worker_src  :self, :blob, assets_host
+    attachments_host = nil
+  end
+
+  data_hosts << attachments_host unless attachments_host.nil?
+
+  if ENV['PAPERCLIP_ROOT_URL']
+    url = Addressable::URI.parse(assets_host) + ENV['PAPERCLIP_ROOT_URL']
+    data_hosts << "https://#{url.host}"
+  end
+
+  data_hosts.concat(ENV['EXTRA_DATA_HOSTS'].split('|')) if ENV['EXTRA_DATA_HOSTS']
+
+  data_hosts.uniq!
+
+  Rails.application.config.content_security_policy do |p|
+    p.base_uri        :none
+    p.default_src     :none
+    p.frame_ancestors :none
+    p.script_src      :self, assets_host
+    p.font_src        :self, assets_host
+    p.img_src         :self, :data, :blob, *data_hosts
+    p.style_src       :self, assets_host
+    p.media_src       :self, :data, *data_hosts
+    p.frame_src       :self, :https
+    p.child_src       :self, :blob, assets_host
+    p.worker_src      :self, :blob, assets_host
+    p.connect_src     :self, :blob, :data, Rails.configuration.x.streaming_api_base_url, *data_hosts
+    p.manifest_src    :self, assets_host
   end
 end
 
diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb
index 55f8c9c91..bc782bc76 100644
--- a/config/initializers/cors.rb
+++ b/config/initializers/cors.rb
@@ -30,5 +30,9 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do
       headers: :any,
       methods: [:post],
       credentials: false
+    resource '/assets/*', headers: :any, methods: [:get, :head, :options]
+    resource '/stylesheets/*', headers: :any, methods: [:get, :head, :options]
+    resource '/javascripts/*', headers: :any, methods: [:get, :head, :options]
+    resource '/packs/*', headers: :any, methods: [:get, :head, :options]
   end
 end
diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb
new file mode 100644
index 000000000..fed182a71
--- /dev/null
+++ b/config/initializers/locale.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+I18n.load_path += Dir[Rails.root.join('app', 'javascript', 'flavours', '*', 'names.{rb,yml}').to_s]
+I18n.load_path += Dir[Rails.root.join('app', 'javascript', 'flavours', '*', 'names', '*.{rb,yml}').to_s]
+I18n.load_path += Dir[Rails.root.join('app', 'javascript', 'skins', '*', '*', 'names.{rb,yml}').to_s]
+I18n.load_path += Dir[Rails.root.join('app', 'javascript', 'skins', '*', '*', 'names', '*.{rb,yml}').to_s]
+I18n.load_path += Dir[Rails.root.join('config', 'locales-glitch', '*.{rb,yml}').to_s]
diff --git a/config/locales-glitch/en.yml b/config/locales-glitch/en.yml
new file mode 100644
index 000000000..5cc2625fc
--- /dev/null
+++ b/config/locales-glitch/en.yml
@@ -0,0 +1,23 @@
+---
+en:
+  admin:
+    settings:
+      enable_keybase:
+        desc_html: Allow your users to prove their identity via keybase
+        title: Enable keybase integration
+      outgoing_spoilers:
+        desc_html: When federating toots, add this content warning to toots that do not have one. It is useful if your server is specialized in content other servers might want to have under a Content Warning. Media will also be marked as sensitive.
+        title: Content warning for outgoing toots
+      hide_followers_count:
+        desc_html: Do not show followers count on user profiles
+        title: Hide followers count
+      show_reblogs_in_public_timelines:
+        desc_html: Show public boosts of public toots in local and public timelines.
+        title: Show boosts in public timelines
+      show_replies_in_public_timelines:
+        desc_html: In addition to public self-replies (threads), show public replies in local and public timelines.
+        title: Show replies in public timelines
+  generic:
+    use_this: Use this
+  settings:
+    flavours: Flavours
diff --git a/config/locales-glitch/es.yml b/config/locales-glitch/es.yml
new file mode 100644
index 000000000..ad942a0c0
--- /dev/null
+++ b/config/locales-glitch/es.yml
@@ -0,0 +1,23 @@
+---
+es:
+  admin:
+    settings:
+      enable_keybase:
+        desc_html: Permite a tus usuarixs comprobar su identidad por medio de keybase
+        title: Habilitar la integración con keybase
+      outgoing_spoilers:
+        desc_html: Cuando los toots federen, agrega esta etiqueta de contenido a los toots que no tengan. Es útil si tu servidor se especializa en contenido que otros servidores desearían tener con una advertencia de contenido. Los medios también se marcarán como sensibles.
+        title: Advertencia de contenido para los toots salientes
+      hide_followers_count:
+        desc_html: No mostrar el conteo de seguidorxs en perfiles de usuarix
+        title: Ocultar conteo de seguidorxs
+      show_reblogs_in_public_timelines:
+        desc_html: Mostrar retoots públicos en las línea de tiempo local y pública.
+        title: Mostrar retoots en líneas de tiempo públicas
+      show_replies_in_public_timelines:
+        desc_html: Además de auto-respuestas públicas (hilos), mostrar respuestas públicas en las línea de tiempo local y pública.
+        title: Mostrar respuestas en líneas de tiempo públicas
+  generic:
+    use_this: Usar
+  settings:
+    flavours: Ediciones
diff --git a/config/locales-glitch/ja.yml b/config/locales-glitch/ja.yml
new file mode 100644
index 000000000..3ecb46ccd
--- /dev/null
+++ b/config/locales-glitch/ja.yml
@@ -0,0 +1,23 @@
+---
+ja:
+  admin:
+    settings:
+      enable_keybase:
+        desc_html: Keybaseにより身元の証明が可能となります
+        title: Keybase統合を有効にする
+      outgoing_spoilers:
+        desc_html: トゥートが連合される際、閲覧注意としてマークされていないトゥートにこの警告が追加されます。これはあなたのインスタンスが他のインスタンスに警告をして欲しいとされる投稿に特化している場合に便利です。 メディアは閲覧注意にマークされます。
+        title: 発信するトゥートへの警告
+      hide_followers_count:
+        desc_html: プロフィールページのフォロワー数を見られないようにします
+        title: フォロワー数を隠す
+      show_reblogs_in_public_timelines:
+        desc_html: ローカルタイムラインと連合タイムラインに公開投稿のブーストを表示します
+        title: 公開タイムラインにブーストを表示
+      show_replies_in_public_timelines:
+        desc_html: 自分への公開投稿の返信に加えて、すべての公開投稿の返信をローカルタイムラインと連合タイムラインに表示します。
+        title: 公開タイムラインに返信を表示
+  generic:
+    use_this: これを使う
+  settings:
+    flavours: フレーバー
diff --git a/config/locales-glitch/ko.yml b/config/locales-glitch/ko.yml
new file mode 100644
index 000000000..aef10f677
--- /dev/null
+++ b/config/locales-glitch/ko.yml
@@ -0,0 +1,23 @@
+---
+ko:
+  admin:
+    settings:
+      enable_keybase:
+        desc_html: 사용자들이 키베이스를 통해 개인 신원을 증명할 수 있도록 허용
+        title: 키베이스 연동 활성화
+      outgoing_spoilers:
+        desc_html: 게시물들을 연합할 때, 열람주의가 달려있지 않다면 이 열람주의를 추가합니다. 다른 서버들이 열람주의를 하길 원하는 콘텐츠들에 특화된 서버에서 유용합니다. 미디어 또한 민감함으로 설정 됩니다.
+        title: 나가는 게시물에 대한 열람주의
+      hide_followers_count:
+        desc_html: 사용자 프로필에 팔로워 수를 표시하지 않습니다
+        title: 팔로워 수 숨기기
+      show_reblogs_in_public_timelines:
+        desc_html: 공개글의 공개적인 부스트를 로컬과 공개 타임라인에 표시합니다.
+        title: 부스트를 공개 타임라인에 표시
+      show_replies_in_public_timelines:
+        desc_html: 자기자신에 대한 답글(글타래)와 마찬가지로, 공개적인 답글을 로컬과 공개 타임라인에 표시합니다.
+        title: 답글을 공개 타임라인에 표시
+  generic:
+    use_this: 사용하기
+  settings:
+    flavours: 풍미
diff --git a/config/locales-glitch/pl.yml b/config/locales-glitch/pl.yml
new file mode 100644
index 000000000..3fcdedcf3
--- /dev/null
+++ b/config/locales-glitch/pl.yml
@@ -0,0 +1,6 @@
+---
+pl:
+  generic:
+    use_this: Użyj tego
+  settings:
+    flavours: Odmiany
diff --git a/config/locales-glitch/simple_form.en.yml b/config/locales-glitch/simple_form.en.yml
new file mode 100644
index 000000000..612943571
--- /dev/null
+++ b/config/locales-glitch/simple_form.en.yml
@@ -0,0 +1,20 @@
+---
+en:
+  simple_form:
+    hints:
+      defaults:
+        setting_default_content_type_html: When writing toots, assume they are written in raw HTML, unless specified otherwise
+        setting_default_content_type_markdown: When writing toots, assume they are using Markdown for rich text formatting, unless specified otherwise
+        setting_default_content_type_plain: When writing toots, assume they are plain text with no special formatting, unless specified otherwise (default Mastodon behavior)
+        setting_default_language: The language of your toots can be detected automatically, but it's not always accurate
+        setting_skin: Reskins the selected Mastodon flavour
+    labels:
+      defaults:
+        setting_default_content_type: Default format for toots
+        setting_default_content_type_html: HTML
+        setting_default_content_type_markdown: Markdown
+        setting_default_content_type_plain: Plain text
+        setting_favourite_modal: Show confirmation dialog before favouriting (applies to Glitch flavour only)
+        setting_hide_followers_count: Hide your followers count
+        setting_skin: Skin
+        setting_system_emoji_font: Use system's default font for emojis (applies to Glitch flavour only)
diff --git a/config/locales-glitch/simple_form.es.yml b/config/locales-glitch/simple_form.es.yml
new file mode 100644
index 000000000..977775be6
--- /dev/null
+++ b/config/locales-glitch/simple_form.es.yml
@@ -0,0 +1,20 @@
+---
+es:
+  simple_form:
+    hints:
+      defaults:
+        setting_default_content_type_html: Al escribir toots, asume que estás escritos en HTML, a menos que se especifique lo contrario
+        setting_default_content_type_markdown: Al escribir toots, asume que estás usando Markdown para dar formato de texto enriquecido, a menos que se especifique lo contrario
+        setting_default_content_type_plain: Al escribir toots, asume que estás usando texto sin formato, a menos que se especifique lo contrario (predeterminado de Mastodon)
+        setting_default_language: El idioma de tus toots se puede detectar automáticamente, pero no siempre es correcto
+        setting_skin: Cambia el diseño de la edición seleccionada de Mastodon
+    labels:
+      defaults:
+        setting_default_content_type: Formato predeterminado de tus toots
+        setting_default_content_type_html: HTML
+        setting_default_content_type_markdown: Markdown
+        setting_default_content_type_plain: Sin formato
+        setting_favourite_modal: Mostrar diálogo de confirmación antes de marcar como favorito (sólo aplica a la edición Glich)
+        setting_hide_followers_count: Ocultar tu conteo de seguidorxs
+        setting_skin: Diseño
+        setting_system_emoji_font: Usar la fuente predeterminada del sistema para emojis (sólo aplica a la edición Glitch)
diff --git a/config/locales-glitch/simple_form.ja.yml b/config/locales-glitch/simple_form.ja.yml
new file mode 100644
index 000000000..dc9e0b579
--- /dev/null
+++ b/config/locales-glitch/simple_form.ja.yml
@@ -0,0 +1,20 @@
+---
+ja:
+  simple_form:
+    hints:
+      defaults:
+        setting_default_content_type_html: トゥートを作成するとき特に指定がない限り生のHTMLで書かれているとします
+        setting_default_content_type_markdown: トゥートを作成するとき特に指定がない限りリッチテキスト形式のマークダウンで書かれているとします
+        setting_default_content_type_plain: トゥートを作成するとき特に指定がない限りプレーンテキストで書かれているとします(Mastodon既定の動作)
+        setting_default_language: あなたのトゥートの言語を自動検出しますが必ずしも正確ではありません
+        setting_skin: 選択したMastodonフレーバーに変更します
+    labels:
+      defaults:
+        setting_default_content_type: 既定のトゥート形式
+        setting_default_content_type_html: HTML
+        setting_default_content_type_markdown: マークダウン
+        setting_default_content_type_plain: プレーンテキスト
+        setting_favourite_modal: お気に入りをする前に確認ダイアログを表示する
+        setting_hide_followers_count: フォロワー数を隠す
+        setting_skin: スキン
+        setting_system_emoji_font: 絵文字にシステム既定のフォントを使用する(Glitch Edition フレーバーのみに適用されます)
diff --git a/config/locales-glitch/simple_form.ko.yml b/config/locales-glitch/simple_form.ko.yml
new file mode 100644
index 000000000..cd9910337
--- /dev/null
+++ b/config/locales-glitch/simple_form.ko.yml
@@ -0,0 +1,20 @@
+---
+ko:
+  simple_form:
+    hints:
+      defaults:
+        setting_default_content_type_html: 게시물을 작성할 때, 형식을 지정하지 않았다면, 생 HTML이라고 가정합니다
+        setting_default_content_type_markdown: 게시물을 작성할 때, 형식을 지정하지 않았다면, 마크다운이라고 가정합니다
+        setting_default_content_type_plain: 게시물을 작성할 때, 형식을 지정하지 않았다면, 일반적인 텍스트라고 가정합니다. (마스토돈의 기본 동작)
+        setting_default_language: 작성하는 게시물의 언어는 자동으로 설정될 수 있습니다, 하지만 언제나 정확하지는 않습니다
+        setting_skin: 선택한 마스토돈 풍미의 스킨을 바꿉니다
+    labels:
+      defaults:
+        setting_default_content_type: 게시물의 기본 포맷
+        setting_default_content_type_html: HTML
+        setting_default_content_type_markdown: 마크다운
+        setting_default_content_type_plain: 일반 텍스트
+        setting_favourite_modal: 관심글을 지정할 때 확인 창을 띄웁니다(글리치 풍미에만 적용됨)
+        setting_hide_followers_count: 내 팔로워 수 숨기기
+        setting_skin: 스킨
+        setting_system_emoji_font: 에모지에 시스템 기본 폰트 적용하기 (글리치 풍미에만 적용됨)
diff --git a/config/locales-glitch/simple_form.pl.yml b/config/locales-glitch/simple_form.pl.yml
new file mode 100644
index 000000000..264494c2d
--- /dev/null
+++ b/config/locales-glitch/simple_form.pl.yml
@@ -0,0 +1,10 @@
+---
+pl:
+  simple_form:
+    hints:
+      defaults:
+        setting_skin: Zmienia wygląd używanej odmiany Mastodona
+    labels:
+      defaults:
+        setting_favourite_modal: Pytaj o potwierdzenie przed dodaniem do ulubionych
+        setting_skin: Motyw
diff --git a/config/locales-glitch/simple_form.zh-CN.yml b/config/locales-glitch/simple_form.zh-CN.yml
new file mode 100644
index 000000000..a82617fdb
--- /dev/null
+++ b/config/locales-glitch/simple_form.zh-CN.yml
@@ -0,0 +1,20 @@
+---
+zh-CN:
+  simple_form:
+    hints:
+      defaults:
+        setting_default_content_type_html: 在撰写嘟文时,除非另有指定,假定它们使用原始 HTML 语言撰写
+        setting_default_content_type_markdown: 在撰写嘟文时,除非另有指定,假定它们使用 Markdown 进行富文本格式化
+        setting_default_content_type_plain: 在撰写嘟文时,除非另有指定,假定它们是没有特殊格式的纯文本(默认的 Mastodon 行为)
+        setting_default_language: 你的嘟文语言可以自动检测,但不一定准确
+        setting_skin: 更换为所选择的 Mastodon 风味
+    labels:
+      defaults:
+        setting_default_content_type: 嘟文的默认格式
+        setting_default_content_type_html: HTML
+        setting_default_content_type_markdown: Markdown
+        setting_default_content_type_plain: 纯文本
+        setting_favourite_modal: 在喜欢嘟文前询问我 (仅限于 Glitch 风味)
+        setting_hide_followers_count: 隐藏你的关注者人数
+        setting_skin: 皮肤
+        setting_system_emoji_font: 表情符号使用系统默认字体 (仅限于 Glitch 风味)
\ No newline at end of file
diff --git a/config/locales-glitch/zh-CN.yml b/config/locales-glitch/zh-CN.yml
new file mode 100644
index 000000000..37a8307e8
--- /dev/null
+++ b/config/locales-glitch/zh-CN.yml
@@ -0,0 +1,23 @@
+---
+zh-CN:
+  admin:
+    settings:
+      enable_keybase:
+        desc_html: 允许你的用户使用 Keybase 证明身份
+        title: 启用 Keybase 集成
+      outgoing_spoilers:
+        desc_html: 在联邦化嘟文的时候,将这个内容警告添加到没有内容警告的嘟文中。如果你的服务器专用于其他服务器可能希望有内容警告的内容,它会很有用。媒体也将被标记为敏感。
+        title: 对外嘟文的内容警告
+      hide_followers_count:
+        desc_html: 不要在用户资料中显示关注者人数
+        title: 隐藏关注者人数
+      show_reblogs_in_public_timelines:
+        desc_html: 在本地和跨站时间线中显示公开嘟文的公开转嘟。
+        title: 在公共时间线中显示转嘟
+      show_replies_in_public_timelines:
+        desc_html: 除了公开的自我回复(线程模式),在本地和跨站时间轴中显示公开回复。
+        title: 在公共时间轴中显示回复
+  generic:
+    use_this: 使用这个
+  settings:
+    flavours: 风味
diff --git a/config/navigation.rb b/config/navigation.rb
index 99743c222..53ee3d6c1 100644
--- a/config/navigation.rb
+++ b/config/navigation.rb
@@ -15,6 +15,12 @@ SimpleNavigation::Configuration.run do |navigation|
       s.item :other, safe_join([fa_icon('cog fw'), t('preferences.other')]), settings_preferences_other_url
     end
 
+    n.item :flavours, safe_join([fa_icon('paint-brush fw'), t('settings.flavours')]), settings_flavours_url do |flavours|
+      Themes.instance.flavours.each do |flavour|
+        flavours.item flavour.to_sym, safe_join([fa_icon('star fw'), t("flavours.#{flavour}.name", default: flavour)]), settings_flavour_url(flavour)
+      end
+    end
+
     n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_url, if: -> { current_user.functional? }
     n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? }
     n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_url, if: -> { current_user.functional? }
diff --git a/config/routes.rb b/config/routes.rb
index 5f73129ea..19d87e6d5 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -151,6 +151,8 @@ Rails.application.routes.draw do
       end
     end
 
+    resources :flavours, only: [:index, :show, :update], param: :flavour
+
     resource :delete, only: [:show, :destroy]
     resource :migration, only: [:show, :create]
 
@@ -357,6 +359,7 @@ Rails.application.routes.draw do
       end
 
       namespace :timelines do
+        resource :direct, only: :show, controller: :direct
         resource :home, only: :show, controller: :home
         resource :public, only: :show, controller: :public
         resources :tag, only: :show
@@ -444,9 +447,10 @@ Rails.application.routes.draw do
         end
       end
 
-      resources :notifications, only: [:index, :show] do
+      resources :notifications, only: [:index, :show, :destroy] do
         collection do
           post :clear
+          delete :destroy_multiple
         end
 
         member do
diff --git a/config/settings.yml b/config/settings.yml
index 06cee2532..094209822 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -2,7 +2,7 @@
 # important settings can be changed from the admin interface.
 
 defaults: &defaults
-  site_title: Mastodon
+  site_title: 'Mastodon Glitch Edition'
   site_short_description: ''
   site_description: ''
   site_extended_description: ''
@@ -13,13 +13,14 @@ defaults: &defaults
   profile_directory: true
   closed_registrations_message: ''
   open_deletion: true
+  timeline_preview: false
   min_invite_role: 'admin'
-  timeline_preview: true
   show_staff_badge: true
   default_sensitive: false
   hide_network: false
   unfollow_modal: false
   boost_modal: false
+  favourite_modal: false
   delete_modal: true
   auto_play_gif: false
   display_media: 'default'
@@ -27,10 +28,13 @@ defaults: &defaults
   preview_sensitive_media: false
   reduce_motion: false
   disable_swiping: false
-  show_application: true
+  show_application: false
   system_font_ui: false
+  system_emoji_font: false
   noindex: false
-  theme: 'default'
+  hide_followers_count: false
+  flavour: 'glitch'
+  skin: 'default'
   aggregate_reblogs: true
   advanced_layout: false
   use_blurhash: true
@@ -66,8 +70,12 @@ defaults: &defaults
   activity_api_enabled: true
   peers_api_enabled: true
   show_known_fediverse_at_about_page: true
+  show_reblogs_in_public_timelines: false
+  show_replies_in_public_timelines: false
+  default_content_type: 'text/plain'
   show_domain_blocks: 'disabled'
   show_domain_blocks_rationale: 'disabled'
+  outgoing_spoilers: ''
   require_invite_text: false
 
 development:
diff --git a/config/themes.yml b/config/themes.yml
deleted file mode 100644
index 9c21c9459..000000000
--- a/config/themes.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-default: styles/application.scss
-contrast: styles/contrast.scss
-mastodon-light: styles/mastodon-light.scss
diff --git a/config/webpack/configuration.js b/config/webpack/configuration.js
index 25b6b7abd..f05c888d5 100644
--- a/config/webpack/configuration.js
+++ b/config/webpack/configuration.js
@@ -1,15 +1,61 @@
 // Common configuration for webpacker loaded from config/webpacker.yml
 
-const { resolve } = require('path');
+const { basename, dirname, extname, join, resolve } = require('path');
 const { env } = require('process');
 const { load } = require('js-yaml');
-const { readFileSync } = require('fs');
+const { lstatSync, readFileSync } = require('fs');
+const glob = require('glob');
 
 const configPath = resolve('config', 'webpacker.yml');
 const settings = load(readFileSync(configPath), 'utf8')[env.RAILS_ENV || env.NODE_ENV];
+const flavourFiles = glob.sync('app/javascript/flavours/*/theme.yml');
+const skinFiles = glob.sync('app/javascript/skins/*/*');
+const flavours = {};
 
-const themePath = resolve('config', 'themes.yml');
-const themes = load(readFileSync(themePath), 'utf8');
+const core = function () {
+  const coreFile = resolve('app', 'javascript', 'core', 'theme.yml');
+  const data = load(readFileSync(coreFile), 'utf8');
+  if (!data.pack_directory) {
+    data.pack_directory = dirname(coreFile);
+  }
+  return data.pack ? data : {};
+}();
+
+for (let i = 0; i < flavourFiles.length; i++) {
+  const flavourFile = flavourFiles[i];
+  const data = load(readFileSync(flavourFile), 'utf8');
+  data.name = basename(dirname(flavourFile));
+  data.skin = {};
+  if (!data.pack_directory) {
+    data.pack_directory = dirname(flavourFile);
+  }
+  if (data.locales) {
+    data.locales = join(dirname(flavourFile), data.locales);
+  }
+  if (data.pack && typeof data.pack === 'object') {
+    flavours[data.name] = data;
+  }
+}
+
+for (let i = 0; i < skinFiles.length; i++) {
+  const skinFile = skinFiles[i];
+  let skin = basename(skinFile);
+  const name = basename(dirname(skinFile));
+  if (!flavours[name]) {
+    continue;
+  }
+  const data = flavours[name].skin;
+  if (lstatSync(skinFile).isDirectory()) {
+    data[skin] = {};
+    const skinPacks = glob.sync(join(skinFile, '*.{css,scss}'));
+    for (let j = 0; j < skinPacks.length; j++) {
+      const pack = skinPacks[j];
+      data[skin][basename(pack, extname(pack))] = pack;
+    }
+  } else if ((skin = skin.match(/^(.*)\.s?css$/i))) {
+    data[skin[1]] = { common: skinFile };
+  }
+}
 
 const output = {
   path: resolve('public', settings.public_output_path),
@@ -18,7 +64,8 @@ const output = {
 
 module.exports = {
   settings,
-  themes,
+  core,
+  flavours,
   env: {
     NODE_ENV: env.NODE_ENV,
     PUBLIC_OUTPUT_PATH: settings.public_output_path,
diff --git a/config/webpack/generateLocalePacks.js b/config/webpack/generateLocalePacks.js
index b71cf2ade..09fba4a18 100644
--- a/config/webpack/generateLocalePacks.js
+++ b/config/webpack/generateLocalePacks.js
@@ -1,52 +1,66 @@
+// A message from upstream:
+// ========================
 // To avoid adding a lot of boilerplate, locale packs are
 // automatically generated here. These are written into the tmp/
 // directory and then used to generate locale_en.js, locale_fr.js, etc.
 
-const fs = require('fs');
-const path = require('path');
+// Glitch note:
+// ============
+// This code has been entirely rewritten to support glitch flavours.
+// However, the underlying process is exactly the same.
+
+const { existsSync, readdirSync, writeFileSync } = require('fs');
+const { join, resolve } = require('path');
 const rimraf = require('rimraf');
 const mkdirp = require('mkdirp');
+const { flavours } = require('./configuration.js');
+
+module.exports = Object.keys(flavours).reduce(function (map, entry) {
+  const flavour = flavours[entry];
+  if (!flavour.locales) {
+    return map;
+  }
+  const locales = readdirSync(flavour.locales).filter(
+    filename => /\.js(?:on)?$/.test(filename) && !/defaultMessages|whitelist|index/.test(filename)
+  );
+  const outPath = resolve('tmp', 'locales', entry);
 
-const localesJsonPath = path.join(__dirname, '../../app/javascript/mastodon/locales');
-const locales = fs.readdirSync(localesJsonPath).filter(filename => {
-  return /\.json$/.test(filename) &&
-    !/defaultMessages/.test(filename) &&
-    !/whitelist/.test(filename);
-}).map(filename => filename.replace(/\.json$/, ''));
-
-const outPath = path.join(__dirname, '../../tmp/packs');
-
-rimraf.sync(outPath);
-mkdirp.sync(outPath);
-
-const outPaths = [];
-
-locales.forEach(locale => {
-  const localePath = path.join(outPath, `locale_${locale}.js`);
-  const baseLocale = locale.split('-')[0]; // e.g. 'zh-TW' -> 'zh'
-  const localeDataPath = [
-    // first try react-intl
-    `../../node_modules/react-intl/locale-data/${baseLocale}.js`,
-    // then check locales/locale-data
-    `../../app/javascript/mastodon/locales/locale-data/${baseLocale}.js`,
-    // fall back to English (this is what react-intl does anyway)
-    '../../node_modules/react-intl/locale-data/en.js',
-  ].filter(filename => fs.existsSync(path.join(outPath, filename)))
-    .map(filename => filename.replace(/..\/..\/node_modules\//, ''))[0];
-
-  const localeContent = `//
-// locale_${locale}.js
+  rimraf.sync(outPath);
+  mkdirp.sync(outPath);
+
+  locales.forEach(function (locale) {
+    const localeName = locale.replace(/\.js(?:on)?$/, '');
+    const localePath = join(outPath, `${localeName}.js`);
+    const baseLocale = localeName.split('-')[0]; // e.g. 'zh-TW' -> 'zh'
+    const localeDataPath = [
+      // first try react-intl
+      `node_modules/react-intl/locale-data/${baseLocale}.js`,
+      // then check locales/locale-data
+      `app/javascript/locales/locale-data/${baseLocale}.js`,
+      // fall back to English (this is what react-intl does anyway)
+      'node_modules/react-intl/locale-data/en.js',
+    ].filter(
+      filename => existsSync(filename)
+    ).map(
+      filename => filename.replace(/(?:node_modules|app\/javascript)\//, '')
+    )[0];
+    const localeContent = `//
+// locales/${entry}/${localeName}.js
 // automatically generated by generateLocalePacks.js
 //
-import messages from '../../app/javascript/mastodon/locales/${locale}.json';
-import localeData from ${JSON.stringify(localeDataPath)};
-import { setLocale } from '../../app/javascript/mastodon/locales';
-setLocale({messages, localeData});
-`;
-  fs.writeFileSync(localePath, localeContent, 'utf8');
-  outPaths.push(localePath);
-});
 
-module.exports = outPaths;
+import messages from '../../../${flavour.locales}/${locale.replace(/\.js$/, '')}';
+import localeData from '${localeDataPath}';
+import { setLocale } from 'locales';
 
+setLocale({
+  localeData,
+  messages,
+});
+`;
+    writeFileSync(localePath, localeContent, 'utf8');
+    map[`locales/${entry}/${localeName}`] = localePath;
+  });
 
+  return map;
+}, {});
diff --git a/config/webpack/rules/babel.js b/config/webpack/rules/babel.js
index 2fc245c43..4d25748ee 100644
--- a/config/webpack/rules/babel.js
+++ b/config/webpack/rules/babel.js
@@ -12,6 +12,7 @@ module.exports = {
     {
       loader: 'babel-loader',
       options: {
+        sourceRoot: 'app/javascript',
         cacheDirectory: join(settings.cache_path, 'babel-loader'),
         cacheCompression: env.NODE_ENV === 'production',
         compact: env.NODE_ENV === 'production',
diff --git a/config/webpack/rules/css.js b/config/webpack/rules/css.js
index bc1f42c13..6ecfb3164 100644
--- a/config/webpack/rules/css.js
+++ b/config/webpack/rules/css.js
@@ -20,6 +20,9 @@ module.exports = {
     {
       loader: 'sass-loader',
       options: {
+        sassOptions: {
+          includePaths: ['app/javascript'],
+        },
         implementation: require('sass'),
         sourceMap: true,
       },
diff --git a/config/webpack/shared.js b/config/webpack/shared.js
index 05828aebe..ce08ac206 100644
--- a/config/webpack/shared.js
+++ b/config/webpack/shared.js
@@ -5,33 +5,56 @@ const { basename, dirname, join, relative, resolve } = require('path');
 const { sync } = require('glob');
 const MiniCssExtractPlugin = require('mini-css-extract-plugin');
 const AssetsManifestPlugin = require('webpack-assets-manifest');
-const extname = require('path-complete-extname');
-const { env, settings, themes, output } = require('./configuration');
+const { env, settings, core, flavours, output } = require('./configuration.js');
 const rules = require('./rules');
-const localePackPaths = require('./generateLocalePacks');
+const localePacks = require('./generateLocalePacks');
+
+function reducePacks (data, into = {}) {
+  if (!data.pack) {
+    return into;
+  }
+  Object.keys(data.pack).reduce((map, entry) => {
+    const pack = data.pack[entry];
+    if (!pack) {
+      return map;
+    }
+    const packFile = typeof pack === 'string' ? pack : pack.filename;
+    if (packFile) {
+      map[data.name ? `flavours/${data.name}/${entry}` : `core/${entry}`] = resolve(data.pack_directory, packFile);
+    }
+    return map;
+  }, into);
+  if (data.name) {
+    Object.keys(data.skin).reduce((map, entry) => {
+      const skin = data.skin[entry];
+      const skinName = entry;
+      if (!skin) {
+        return map;
+      }
+      Object.keys(skin).reduce((map, entry) => {
+        const packFile = skin[entry];
+        if (!packFile) {
+          return map;
+        }
+        map[`skins/${data.name}/${skinName}/${entry}`] = resolve(packFile);
+        return map;
+      }, into);
+      return map;
+    }, into);
+  }
+  return into;
+}
+
+const entries = Object.assign(
+  { locales: resolve('app', 'javascript', 'locales') },
+  localePacks,
+  reducePacks(core),
+  Object.keys(flavours).reduce((map, entry) => reducePacks(flavours[entry], map), {})
+);
 
-const extensionGlob = `**/*{${settings.extensions.join(',')}}*`;
-const entryPath = join(settings.source_path, settings.source_entry_path);
-const packPaths = sync(join(entryPath, extensionGlob));
 
 module.exports = {
-  entry: Object.assign(
-    packPaths.reduce((map, entry) => {
-      const localMap = map;
-      const namespace = relative(join(entryPath), dirname(entry));
-      localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry);
-      return localMap;
-    }, {}),
-    localePackPaths.reduce((map, entry) => {
-      const localMap = map;
-      localMap[basename(entry, extname(entry, extname(entry)))] = resolve(entry);
-      return localMap;
-    }, {}),
-    Object.keys(themes).reduce((themePaths, name) => {
-      themePaths[name] = resolve(join(settings.source_path, themes[name]));
-      return themePaths;
-    }, {}),
-  ),
+  entry: entries,
 
   output: {
     filename: 'js/[name]-[chunkhash].js',
@@ -43,7 +66,7 @@ module.exports = {
 
   optimization: {
     runtimeChunk: {
-      name: 'common',
+      name: 'locales',
     },
     splitChunks: {
       cacheGroups: {
@@ -51,7 +74,9 @@ module.exports = {
         vendors: false,
         common: {
           name: 'common',
-          chunks: 'all',
+          chunks (chunk) {
+            return !(chunk.name in entries);
+          },
           minChunks: 2,
           minSize: 0,
           test: /^(?!.*[\\\/]node_modules[\\\/]react-intl[\\\/]).+$/,
diff --git a/config/webpacker.yml b/config/webpacker.yml
index 4ad78a190..9accd6152 100644
--- a/config/webpacker.yml
+++ b/config/webpacker.yml
@@ -26,6 +26,7 @@ default: &default
     - .tiff
     - .ico
     - .svg
+    - .gif
     - .eot
     - .otf
     - .ttf