]> cat aescling's git repositories - mastodon.git/commitdiff
Merge branch 'master' into glitch-soc/merge-upstream
authorThibaut Girka <thib@sitedethib.com>
Mon, 19 Aug 2019 17:31:32 +0000 (19:31 +0200)
committerThibaut Girka <thib@sitedethib.com>
Mon, 19 Aug 2019 19:49:35 +0000 (21:49 +0200)
Conflicts:
- app/controllers/home_controller.rb
- app/controllers/shares_controller.rb
- app/javascript/packs/public.js
- app/models/status.rb
- app/serializers/initial_state_serializer.rb
- app/views/home/index.html.haml
- app/views/layouts/public.html.haml
- app/views/public_timelines/show.html.haml
- app/views/shares/show.html.haml
- app/views/tags/show.html.haml
- config/initializers/content_security_policy.rb
- config/locales/en.yml
- config/webpack/shared.js
- package.json

45 files changed:
1  2 
Gemfile
Gemfile.lock
app/controllers/about_controller.rb
app/controllers/accounts_controller.rb
app/controllers/application_controller.rb
app/controllers/follower_accounts_controller.rb
app/controllers/following_accounts_controller.rb
app/controllers/home_controller.rb
app/controllers/invites_controller.rb
app/controllers/public_timelines_controller.rb
app/controllers/shares_controller.rb
app/controllers/tags_controller.rb
app/helpers/application_helper.rb
app/javascript/mastodon/initial_state.js
app/javascript/mastodon/locales/defaultMessages.json
app/javascript/mastodon/locales/en.json
app/javascript/mastodon/locales/ja.json
app/javascript/mastodon/locales/pl.json
app/javascript/packs/public.js
app/javascript/styles/mastodon/components.scss
app/models/account.rb
app/models/form/admin_settings.rb
app/models/media_attachment.rb
app/models/status.rb
app/serializers/activitypub/note_serializer.rb
app/serializers/initial_state_serializer.rb
app/views/admin/settings/edit.html.haml
app/views/home/index.html.haml
app/views/layouts/public.html.haml
app/views/public_timelines/show.html.haml
app/views/shares/show.html.haml
app/views/tags/show.html.haml
config/environments/production.rb
config/initializers/content_security_policy.rb
config/locales/en.yml
config/locales/ja.yml
config/locales/pl.yml
config/locales/simple_form.en.yml
config/locales/simple_form.ja.yml
config/routes.rb
config/settings.yml
config/webpack/shared.js
db/schema.rb
package.json
yarn.lock

diff --cc Gemfile
Simple merge
diff --cc Gemfile.lock
Simple merge
index f41e52aae9449304c7139ec1449dfc0e2eb03da1,5e942e5c07ca591fbe8d46979c916d292487bce9..5003ae61cc27fa870d6c0f12ab5ead7a3ff4294a
@@@ -1,13 -1,14 +1,15 @@@
  # frozen_string_literal: true
  
  class AboutController < ApplicationController
 +  before_action :set_pack
    layout 'public'
  
-   before_action :require_open_federation!, only: [:show, :more]
+   before_action :require_open_federation!, only: [:show, :more, :blocks]
+   before_action :check_blocklist_enabled, only: [:blocks]
+   before_action :authenticate_user!, only: [:blocks], if: :blocklist_account_required?
    before_action :set_body_classes, only: :show
    before_action :set_instance_presenter
-   before_action :set_expires_in
+   before_action :set_expires_in, only: [:show, :more, :terms]
  
    skip_before_action :require_functional!, only: [:more, :terms]
  
index a09aed80142057cb7efed4865329faa627ef869f,7c8a18d17cc48a452cccb5ba804887e3f390d06a..efdb1d2268ae21a45386eafa55b6b11ba548c379
@@@ -2,10 -2,7 +2,9 @@@
  
  class HomeController < ApplicationController
    before_action :authenticate_user!
 +
 +  before_action :set_pack
    before_action :set_referrer_policy_header
-   before_action :set_initial_state_json
  
    def index
      @body_classes = 'app-body'
      redirect_to(matches ? tag_path(CGI.unescape(matches[:tag])) : default_redirect_path)
    end
  
-   def set_initial_state_json
-     serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
-     @initial_state_json   = serializable_resource.to_json
-   end
-   def initial_state_params
-     {
-       settings: Web::Setting.find_by(user: current_user)&.data || {},
-       push_subscription: current_account.user.web_push_subscription(current_session),
-       current_account: current_account,
-       token: current_session.token,
-       admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
-     }
-   end
 +  def set_pack
 +    use_pack 'home'
 +  end
 +
    def default_redirect_path
      if request.path.start_with?('/web') || whitelist_mode?
        new_user_session_path
Simple merge
index ada4eec54514dadf9908487c9f26c7dc502a47f0,6546b8497808c46e4ff95dbcf3a87ebb50d4f146..e13e7e8b656014243a5a1811f2c8bb6e78c1a8ec
@@@ -4,33 -4,12 +4,17 @@@ class SharesController < ApplicationCon
    layout 'modal'
  
    before_action :authenticate_user!
 +  before_action :set_pack
    before_action :set_body_classes
  
-   def show
-     serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer)
-     @initial_state_json   = serializable_resource.to_json
-   end
+   def show; end
  
    private
  
-   def initial_state_params
-     text = [params[:title], params[:text], params[:url]].compact.join(' ')
-     {
-       settings: Web::Setting.find_by(user: current_user)&.data || {},
-       push_subscription: current_account.user.web_push_subscription(current_session),
-       current_account: current_account,
-       token: current_session.token,
-       admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')),
-       text: text,
-     }
-   end
 +  def set_pack
 +    use_pack 'share'
 +  end
 +
    def set_body_classes
      @body_classes = 'modal-layout compose-standalone'
    end
index d6bb28eb51329e5d786fdb5084dce74952462db3,4dfa0526413579063001824e4952d058a1c3fb5b..c447a3a2b83486fe1c8d4299c3cdebc580329487
@@@ -16,13 -16,7 +16,8 @@@ class TagsController < ApplicationContr
    def show
      respond_to do |format|
        format.html do
 +        use_pack 'about'
          expires_in 0, public: true
-         @initial_state_json = ActiveModelSerializers::SerializableResource.new(
-           InitialStatePresenter.new(settings: {}, token: current_session&.token),
-           serializer: InitialStateSerializer
-         ).to_json
        end
  
        format.rss do
Simple merge
index 3c6d718350b7afd01cdf92cb4b0094e5c03dd404,352a00c1ef11124dcce7983abf099de14829263c..d28fe4247c905cd0faf806acec556ff2cdd68a01
    "navigation_bar.preferences": "ユーザー設定",
    "navigation_bar.profile_directory": "ディレクトリ",
    "navigation_bar.public_timeline": "連合タイムライン",
 +  "navigation_bar.misc": "その他",
    "navigation_bar.security": "セキュリティ",
+   "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
    "notification.favourite": "{name}さんがあなたのトゥートをお気に入りに登録しました",
    "notification.follow": "{name}さんにフォローされました",
    "notification.mention": "{name}さんがあなたに返信しました",
index 6aea119e37b241ecbfd774ed8c1403877ef7ef09,c5cd7129f01460a49989fc0c36c8ad9b390ee955..e49dcaadb875165d837d9b6be7c930e23421a048
@@@ -100,6 -117,135 +100,15 @@@ function main() 
  
      delegate(document, '.custom-emoji', 'mouseover', getEmojiAnimationHandler('data-original'));
      delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static'));
 -  });
 -
 -  delegate(document, '.webapp-btn', 'click', ({ target, button }) => {
 -    if (button !== 0) {
 -      return true;
 -    }
 -    window.location.href = target.href;
 -    return false;
 -  });
 -
 -  delegate(document, '.status__content__spoiler-link', 'click', function() {
 -    const contentEl = this.parentNode.parentNode.querySelector('.e-content');
 -
 -    if (contentEl.style.display === 'block') {
 -      contentEl.style.display = 'none';
 -      this.parentNode.style.marginBottom = 0;
 -    } else {
 -      contentEl.style.display = 'block';
 -      this.parentNode.style.marginBottom = null;
 -    }
 -
 -    return false;
 -  });
 -
 -  delegate(document, '.blocks-table button.icon-button', 'click', function(e) {
 -    e.preventDefault();
 -
 -    const classList = this.firstElementChild.classList;
 -    classList.toggle('fa-chevron-down');
 -    classList.toggle('fa-chevron-up');
 -    this.parentElement.parentElement.nextElementSibling.classList.toggle('hidden');
 -  });
 -
 -  delegate(document, '.modal-button', 'click', e => {
 -    e.preventDefault();
 -
 -    let href;
 -
 -    if (e.target.nodeName !== 'A') {
 -      href = e.target.parentNode.href;
 -    } else {
 -      href = e.target.href;
 -    }
 -
 -    window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
 -  });
 -
 -  delegate(document, '#account_display_name', 'input', ({ target }) => {
 -    const name = document.querySelector('.card .display-name strong');
 -    if (name) {
 -      if (target.value) {
 -        name.innerHTML = emojify(escapeTextContentForBrowser(target.value));
 -      } else {
 -        name.textContent = document.querySelector('#default_account_display_name').textContent;
 -      }
 -    }
 -  });
 -
 -  delegate(document, '#account_avatar', 'change', ({ target }) => {
 -    const avatar = document.querySelector('.card .avatar img');
 -    const [file] = target.files || [];
 -    const url = file ? URL.createObjectURL(file) : avatar.dataset.originalSrc;
 -
 -    avatar.src = url;
 -  });
 -
 -  const getProfileAvatarAnimationHandler = (swapTo) => {
 -    //animate avatar gifs on the profile page when moused over
 -    return ({ target }) => {
 -      const swapSrc = target.getAttribute(swapTo);
 -      //only change the img source if autoplay is off and the image src is actually different
 -      if(target.getAttribute('data-autoplay') !== 'true' && target.src !== swapSrc) {
 -        target.src = swapSrc;
 -      }
 -    };
 -  };
 -
 -  delegate(document, 'img#profile_page_avatar', 'mouseover', getProfileAvatarAnimationHandler('data-original'));
 -
 -  delegate(document, 'img#profile_page_avatar', 'mouseout', getProfileAvatarAnimationHandler('data-static'));
 -
 -  delegate(document, '#account_header', 'change', ({ target }) => {
 -    const header = document.querySelector('.card .card__img img');
 -    const [file] = target.files || [];
 -    const url = file ? URL.createObjectURL(file) : header.dataset.originalSrc;
 -
 -    header.src = url;
 -  });
 -  delegate(document, '#account_locked', 'change', ({ target }) => {
 -    const lock = document.querySelector('.card .display-name i');
++    delegate(document, '.blocks-table button.icon-button', 'click', function(e) {
++      e.preventDefault();
 -    if (target.checked) {
 -      lock.style.display = 'inline';
 -    } else {
 -      lock.style.display = 'none';
 -    }
 -  });
 -
 -  delegate(document, '.input-copy input', 'click', ({ target }) => {
 -    target.focus();
 -    target.select();
 -    target.setSelectionRange(0, target.value.length);
 -  });
 -
 -  delegate(document, '.input-copy button', 'click', ({ target }) => {
 -    const input = target.parentNode.querySelector('.input-copy__wrapper input');
 -
 -    const oldReadOnly = input.readonly;
 -
 -    input.readonly = false;
 -    input.focus();
 -    input.select();
 -    input.setSelectionRange(0, input.value.length);
 -
 -    try {
 -      if (document.execCommand('copy')) {
 -        input.blur();
 -        target.parentNode.classList.add('copied');
 -
 -        setTimeout(() => {
 -          target.parentNode.classList.remove('copied');
 -        }, 700);
 -      }
 -    } catch (err) {
 -      console.error(err);
 -    }
 -
 -    input.readonly = oldReadOnly;
++      const classList = this.firstElementChild.classList;
++      classList.toggle('fa-chevron-down');
++      classList.toggle('fa-chevron-up');
++      this.parentElement.parentElement.nextElementSibling.classList.toggle('hidden');
++    });
    });
  }
  
Simple merge
index 2c3a7f13b955ca78c7c67a8c2436d0290cfb10cf,6bc3ca9f528c4440498b525ba5987920b25c9e5b..57dd3edd9c4dafbd2e31404309ac46568e9c4c07
@@@ -32,10 -28,10 +32,12 @@@ class Form::AdminSetting
      thumbnail
      hero
      mascot
 +    show_reblogs_in_public_timelines
 +    show_replies_in_public_timelines
      spam_check_enabled
      trends
+     show_domain_blocks
+     show_domain_blocks_rationale
    ).freeze
  
    BOOLEAN_KEYS = %i(
Simple merge
index 642d3cf5e8609c991e4fa006a0ba506e71205f82,0538c4e9e2a1128abe7013ebcd58d44ab1cd9377..de790027d5b766a1bf1824213f552ad765d2f06c
@@@ -446,15 -388,10 +446,19 @@@ class Status < ApplicationRecor
      end
    end
  
 +  def marked_local_only?
 +    # match both with and without U+FE0F (the emoji variation selector)
 +    /#{local_only_emoji}\ufe0f?\z/.match?(content)
 +  end
 +
 +  def local_only_emoji
 +    '👁'
 +  end
 +
+   def status_stat
+     super || build_status_stat
+   end
    private
  
    def update_status_stat!(attrs)
index c8f6bec7a567cc3132a001d810cc4fe227208437,fb53ea3145e8449e64d0992f5c35818db2ef330a..d40fe3380601c3c543727faa481997bca049690d
@@@ -52,8 -38,11 +53,13 @@@ class InitialStateSerializer < ActiveMo
        store[:use_pending_items] = object.current_account.user.setting_use_pending_items
        store[:is_staff]          = object.current_account.user.staff?
        store[:trends]            = Setting.trends && object.current_account.user.setting_trends
 +      store[:default_content_type] = object.current_account.user.setting_default_content_type
 +      store[:system_emoji_font] = object.current_account.user.setting_system_emoji_font
+     else
+       store[:auto_play_gif] = Setting.auto_play_gif
+       store[:display_media] = Setting.display_media
+       store[:reduce_motion] = Setting.reduce_motion
+       store[:use_blurhash]  = Setting.use_blurhash
      end
  
      store
index 6c5268b61e7cb93366f6454cc2cef24cf5a5ce5d,30c7aab194f3aa6365f1839ebe5634cfbc184051..9530e612aa72556587a528fcc44494bb7acbf47d
@@@ -5,7 -5,8 +5,7 @@@
    = preload_link_tag asset_pack_path('features/notifications.js'), crossorigin: 'anonymous'
  
    %meta{name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key}
-   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
+   = render_initial_state
 -  = javascript_pack_tag 'application', integrity: true, crossorigin: 'anonymous'
  
  .app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
    %noscript
index da510fa7a2c163561d890f061e8c9257ba9d92c3,b9179e23d17c528ab0530800e64a14e500395d50..fb9ac5cec070004a4e0cf221284f994a392427bc
@@@ -1,3 -1,7 +1,6 @@@
 -  = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous'
+ - content_for :header_tags do
+   = render_initial_state
  - content_for :content do
    .public-layout
      - unless @hide_navbar
index f80157c67a50276960e1d9f89037e5f7f90aa23a,07215efdfdf389dab83c03d409fcac9e3e06223f..591620f6421dc0af8dd31baa60295931aa3090fc
@@@ -3,7 -3,7 +3,6 @@@
  
  - content_for :header_tags do
    %meta{ name: 'robots', content: 'noindex' }/
-   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
 -  = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous'
  
  .page-header
    %h1= t('about.see_whats_happening')
index 4c0390c421330adb4cd9587c67dd1d776c0a7516,f2f5479a79c4a6d9f4331d9f486e37d6ec189e10..28910d3abdfb1d8c2fcc4b1e666aaf73bb57915c
@@@ -1,4 -1,5 +1,4 @@@
  - content_for :header_tags do
-   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
+   = render_initial_state
 -  = javascript_pack_tag 'share', integrity: true, crossorigin: 'anonymous'
  
  #mastodon-compose{ data: { props: Oj.dump(default_props) } }
index 1a9c58983728ced43d493a5d3cbc77fd8e2e9f50,6307022779422530707c1adef13dfcd3d130d169..74d44bd7b29b7fb44569e01fed3ce1b48f8b11fe
@@@ -5,7 -5,7 +5,6 @@@
    %meta{ name: 'robots', content: 'noindex' }/
    %link{ rel: 'alternate', type: 'application/rss+xml', href: tag_url(@tag, format: 'rss') }/
  
-   %script#initial-state{ type: 'application/json' }!= json_escape(@initial_state_json)
 -  = javascript_pack_tag 'about', integrity: true, crossorigin: 'anonymous'
    = render 'og'
  
  .page-header
Simple merge
index 2fe1a33fa5d49f007b5f34b104980db7fb34fbca,2dbc15a8da65da7090547035ef5884e9133cbdd5..c8396c773434f4a1ceef2cf3ea7cb8aa30456569
@@@ -2,42 -2,39 +2,42 @@@
  # 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
 -
 -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
 +if Rails.env.production?
 +  assets_host = Rails.configuration.action_controller.asset_host || "https://#{ENV['WEB_DOMAIN'] || ENV['LOCAL_DOMAIN']}"
 +  data_hosts = [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
 +    attachments_host = nil
 +  end
  
 -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, :unsafe_inline, assets_host
 -  p.media_src       :self, :https, :data, assets_host
 -  p.frame_src       :self, :https
 -  p.manifest_src    :self, assets_host
 +  data_hosts << attachments_host unless attachments_host.nil?
  
 -  if Rails.env.development?
 -    webpacker_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{Webpacker.dev_server.host_with_port}" }
 +  if ENV['PAPERCLIP_ROOT_URL']
 +    url = Addressable::URI.parse(assets_host) + ENV['PAPERCLIP_ROOT_URL']
 +    data_hosts << "https://#{url.host}"
 +  end
  
 -    p.connect_src :self, :data, :blob, assets_host, media_host, Rails.configuration.x.streaming_api_base_url, *webpacker_urls
 -    p.script_src  :self, :blob, :unsafe_inline, :unsafe_eval, assets_host
 -  else
 -    p.connect_src :self, :data, :blob, assets_host, media_host, Rails.configuration.x.streaming_api_base_url
 -    p.script_src  :self, :blob, assets_host
 +  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, :unsafe_inline, assets_host
 +    p.media_src       :self, :data, *data_hosts
 +    p.frame_src       :self, :https
-     p.worker_src      :self, assets_host
++    p.worker_src      :self, :blob, assets_host
 +    p.connect_src     :self, :blob, Rails.configuration.x.streaming_api_base_url, *data_hosts
 +    p.manifest_src    :self, assets_host
    end
  end
  
index 79b8e02bda0b76f2c472c66e41a8a3fae7466f68,8d267065c298bab0fb2ec5ccfa7fdaaf40364595..be190f0f1376c19b9e0c73647c196f1082da81c0
@@@ -422,9 -423,13 +424,16 @@@ en
        custom_css:
          desc_html: Modify the look with CSS loaded on every page
          title: Custom CSS
+       domain_blocks:
+         all: To everyone
+         disabled: To no one
+         title: Show domain blocks
+         users: To logged-in local users
+       domain_blocks_rationale:
+         title: Show rationale
 +      enable_keybase:
 +        desc_html: Allow your users to prove their identity via keybase
 +        title: Enable keybase integration
        hero:
          desc_html: Displayed on the frontpage. At least 600x100px recommended. When not set, falls back to server thumbnail
          title: Hero image
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
index 70a9a3a4458808726afab7ee321b0c0cd8395f36,6dbc46706a1b6068c465897acb97f9b6753c9ad2..0e615f8e8df15d826422b39c781a132976588b5e
@@@ -68,10 -63,9 +68,12 @@@ defaults: &default
    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'
    spam_check_enabled: true
+   show_domain_blocks: 'disabled'
+   show_domain_blocks_rationale: 'disabled'
  
  development:
    <<: *defaults
index b6e2537b1fb128ae9890416b72ea34360d9550de,d5e399ced80a1320c7c9967e6469f90b37136180..1741ffdb97923a71875a74d07f4de7c82ee81f09
@@@ -5,56 -5,34 +5,57 @@@ const { basename, dirname, join, relati
  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 CopyPlugin = require('copy-webpack-plugin');
 +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',
diff --cc db/schema.rb
Simple merge
diff --cc package.json
index 061234a31f98b7d6aaf9a17b7330d2fc8b4a66cc,8f050d042af718021e58d46951af2247d0010208..6bbe87f112d91d04d0e9b5307f0edc652fb5184a
@@@ -96,8 -95,7 +97,8 @@@
      "escape-html": "^1.0.3",
      "exif-js": "^2.3.0",
      "express": "^4.17.1",
-     "file-loader": "^4.1.0",
+     "file-loader": "^4.2.0",
 +    "favico.js": "^0.3.10",
      "font-awesome": "^4.7.0",
      "glob": "^7.1.1",
      "http-link-header": "^1.0.2",
diff --cc yarn.lock
Simple merge