]> cat aescling's git repositories - mastodon.git/commitdiff
Merge branch 'master' into glitch-soc/merge-upstream
authorThibaut Girka <thib@sitedethib.com>
Thu, 10 Jan 2019 18:12:10 +0000 (19:12 +0100)
committerThibaut Girka <thib@sitedethib.com>
Thu, 10 Jan 2019 20:00:30 +0000 (21:00 +0100)
Conflicts:
- .eslintrc.yml
  Removed, as upstream removed it.
- app/controllers/admin/statuses_controller.rb
  Minor code cleanup when porting one of our features.
- app/models/account.rb
  Note length validation has changed upstream.
  We now use upstream's validation (dropped legacy glitch-soc
  account metadata stuff) but with configurable limit.
- app/services/post_status_service.rb
  Upstream has added support for scheduled toots, refactoring
  the code a bit. Adapted our changes to this refactoring.
- app/views/stream_entries/_detailed_status.html.haml
  Not a real conflict, changes too close.
- app/views/stream_entries/_simple_status.html.haml
  Not a real conflict, changes too close.

38 files changed:
1  2 
Gemfile
Gemfile.lock
app/controllers/remote_interaction_controller.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/admin.scss
app/javascript/styles/mastodon/components.scss
app/models/account.rb
app/models/concerns/account_associations.rb
app/models/media_attachment.rb
app/services/post_status_service.rb
app/views/stream_entries/_detailed_status.html.haml
app/views/stream_entries/_simple_status.html.haml
config/locales/ca.yml
config/locales/de.yml
config/locales/en.yml
config/locales/es.yml
config/locales/gl.yml
config/locales/ja.yml
config/locales/nl.yml
config/locales/oc.yml
config/locales/pl.yml
config/locales/pt-BR.yml
config/locales/simple_form.ja.yml
config/locales/simple_form.pl.yml
config/locales/sk.yml
config/navigation.rb
config/routes.rb
config/settings.yml
db/schema.rb
lib/mastodon/version.rb
package.json
spec/lib/feed_manager_spec.rb
yarn.lock

diff --cc Gemfile
Simple merge
diff --cc Gemfile.lock
Simple merge
index 6861f3f21b3398edb1ff3d65bdb2659061be6776,cc6993c52b22236dd4b95a4aa698c5e5527e8eed..d7197d434b7d32c9e179d2ecec48a8d277ad5783
@@@ -5,9 -5,9 +5,10 @@@ class RemoteInteractionController < App
  
    layout 'modal'
  
+   before_action :set_interaction_type
    before_action :set_status
    before_action :set_body_classes
 +  before_action :set_pack
  
    def new
      @remote_follow = RemoteFollow.new(session_params)
      @hide_header  = true
    end
  
 +  def set_pack
 +    use_pack 'modal'
 +  end
++
+   def set_interaction_type
+     @interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply'
+   end
  end
index 47c4bf75ce0477633b3dd080ba07c085d7f20fbc,a9a3d738a9aa5ba7b32d487afc058a8403656999..196d2d02f9d850b2c884cc47c03670d4ccddc701
@@@ -68,16 -102,104 +85,7 @@@ function main() 
      if (parallaxComponents.length > 0 ) {
        new Rellax('.parallax', { speed: -1 });
      }
-     const history = createHistory();
-     const detailedStatuses = document.querySelectorAll('.public-layout .detailed-status');
-     const location = history.location;
-     if (detailedStatuses.length === 1 && (!location.state || !location.state.scrolledToDetailedStatus)) {
-       detailedStatuses[0].scrollIntoView();
-       history.replace(location.pathname, { ...location.state, scrolledToDetailedStatus: true });
-     }
    });
 -
 -  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', ({ target }) => {
 -    const contentEl = target.parentNode.parentNode.querySelector('.e-content');
 -
 -    if (contentEl.style.display === 'block') {
 -      contentEl.style.display = 'none';
 -      target.parentNode.style.marginBottom = 0;
 -    } else {
 -      contentEl.style.display = 'block';
 -      target.parentNode.style.marginBottom = null;
 -    }
 -
 -    return false;
 -  });
 -
 -  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;
 -  });
 -
 -  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');
 -
 -    if (target.checked) {
 -      lock.style.display = 'inline';
 -    } else {
 -      lock.style.display = 'none';
 -    }
 -  });
 -
 -  delegate(document, '.input-copy input', 'click', ({ target }) => {
 -    target.select();
 -  });
 -
 -  delegate(document, '.input-copy button', 'click', ({ target }) => {
 -    const input = target.parentNode.querySelector('.input-copy__wrapper input');
 -
 -    input.focus();
 -    input.select();
 -
 -    try {
 -      if (document.execCommand('copy')) {
 -        input.blur();
 -        target.parentNode.classList.add('copied');
 -
 -        setTimeout(() => {
 -          target.parentNode.classList.remove('copied');
 -        }, 700);
 -      }
 -    } catch (err) {
 -      console.error(err);
 -    }
 -  });
  }
  
  loadPolyfills().then(main).catch(error => {
index 67d9a583ec6eb1b1276fef0303029036eb97c497,11a3c21fe483fa7b146298f5cbd42c3a5e6c6b59..1ee63c7388d3e2a8bca74b7fb7019de1d8e42b4d
@@@ -78,9 -74,9 +78,9 @@@ class Account < ApplicationRecor
    validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? }
    validates_with UniqueUsernameValidator, if: -> { local? && will_save_change_to_username? }
    validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? }
 -  validates :display_name, length: { maximum: 30 }, if: -> { local? && will_save_change_to_display_name? }
 -  validates :note, note_length: { maximum: 160 }, if: -> { local? && will_save_change_to_note? }
 -  validates :fields, length: { maximum: 4 }, if: -> { local? && will_save_change_to_fields? }
 +  validates :display_name, length: { maximum: MAX_DISPLAY_NAME_LENGTH }, if: -> { local? && will_save_change_to_display_name? }
-   validate :note_length_does_not_exceed_length_limit, if: -> { local? && will_save_change_to_note? }
++  validates :note, note_length: { maximum: MAX_NOTE_LENGTH }, if: -> { local? && will_save_change_to_note? }
 +  validates :fields, length: { maximum: MAX_FIELDS }, if: -> { local? && will_save_change_to_fields? }
  
    scope :remote, -> { where.not(domain: nil) }
    scope :local, -> { where(domain: nil) }
Simple merge
index 28ecc848de02f6c253fdf0be0eccf82d18ae60be,260765edfd4a3669c76752828d06ffb04a7a75db..2ca92dc50eb2c813458bb150d4b9c4d362e3bc57
@@@ -13,65 -17,80 +17,85 @@@ class PostStatusService < BaseServic
    # @option [Doorkeeper::Application] :application
    # @option [String] :idempotency Optional idempotency key
    # @return [Status]
-   def call(account, text, in_reply_to = nil, **options)
-     if options[:idempotency].present?
-       existing_id = redis.get("idempotency:status:#{account.id}:#{options[:idempotency]}")
-       return Status.find(existing_id) if existing_id
+   def call(account, options = {})
+     @account     = account
+     @options     = options
+     @text        = @options[:text] || ''
+     @in_reply_to = @options[:thread]
+     return idempotency_duplicate if idempotency_given? && idempotency_duplicate?
+     validate_media!
+     preprocess_attributes!
+     if scheduled?
+       schedule_status!
+     else
+       process_status!
+       postprocess_status!
+       bump_potential_friendship!
      end
  
-     media  = validate_media!(options[:media_ids])
-     status = nil
-     if text.blank? && options[:spoiler_text].present?
-      text = '.'
-      text = media.find(&:video?) ? '📹' : '🖼' if media.size > 0
+     redis.setex(idempotency_key, 3_600, @status.id) if idempotency_given?
+     @status
+   end
+   private
+   def preprocess_attributes!
 -    @text         = @options.delete(:spoiler_text) if @text.blank? && @options[:spoiler_text].present?
++    if @text.blank? && @options[:spoiler_text].present?
++     @text = '.'
++     @text = @media.find(&:video?) ? '📹' : '🖼' if @media.size > 0
 +    end
+     @visibility   = @options[:visibility] || @account.user&.setting_default_privacy
+     @visibility   = :unlisted if @visibility == :public && @account.silenced
+     @scheduled_at = @options[:scheduled_at]&.to_datetime
+     @scheduled_at = nil if scheduled_in_the_past?
+   end
  
-     visibility = options[:visibility] || account.user&.setting_default_privacy
-     visibility = :unlisted if visibility == :public && account.silenced
+   def process_status!
+     # The following transaction block is needed to wrap the UPDATEs to
+     # the media attachments when the status is created
  
      ApplicationRecord.transaction do
-       status = account.statuses.create!(text: text,
-                                         media_attachments: media || [],
-                                         thread: in_reply_to,
-                                         sensitive: (options[:sensitive].nil? ? account.user&.setting_default_sensitive : options[:sensitive]) || options[:spoiler_text].present?,
-                                         spoiler_text: options[:spoiler_text] || '',
-                                         visibility: visibility,
-                                         language: language_from_option(options[:language]) || account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(text, account),
-                                         application: options[:application])
+       @status = @account.statuses.create!(status_attributes)
      end
  
-     process_hashtags_service.call(status)
-     process_mentions_service.call(status)
+     process_hashtags_service.call(@status)
+     process_mentions_service.call(@status)
+   end
  
-     LinkCrawlWorker.perform_async(status.id) unless status.spoiler_text?
-     DistributionWorker.perform_async(status.id)
+   def schedule_status!
+     if @account.statuses.build(status_attributes).valid?
+       # The following transaction block is needed to wrap the UPDATEs to
+       # the media attachments when the scheduled status is created
  
-     unless status.local_only?
-       Pubsubhubbub::DistributionWorker.perform_async(status.stream_entry.id)
-       ActivityPub::DistributionWorker.perform_async(status.id)
+       ApplicationRecord.transaction do
+         @status = @account.scheduled_statuses.create!(scheduled_status_attributes)
+       end
+     else
+       raise ActiveRecord::RecordInvalid
      end
+   end
  
-     if options[:idempotency].present?
-       redis.setex("idempotency:status:#{account.id}:#{options[:idempotency]}", 3_600, status.id)
+   def postprocess_status!
+     LinkCrawlWorker.perform_async(@status.id) unless @status.spoiler_text?
+     DistributionWorker.perform_async(@status.id)
 -    Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id)
 -    ActivityPub::DistributionWorker.perform_async(@status.id)
++    unless @status.local_only?
++      Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id)
++      ActivityPub::DistributionWorker.perform_async(@status.id)
 +    end
-     bump_potential_friendship(account, status)
-     status
    end
  
-   private
-   def validate_media!(media_ids)
-     return if media_ids.blank? || !media_ids.is_a?(Enumerable)
+   def validate_media!
+     return if @options[:media_ids].blank? || !@options[:media_ids].is_a?(Enumerable)
  
-     raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if media_ids.size > 4
+     raise Mastodon::ValidationError, I18n.t('media_attachments.validations.too_many') if @options[:media_ids].size > 4
  
-     media = MediaAttachment.where(status_id: nil).where(id: media_ids.take(4).map(&:to_i))
+     @media = MediaAttachment.where(status_id: nil).where(id: @options[:media_ids].take(4).map(&:to_i))
  
-     raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if media.size > 1 && media.find(&:video?)
-     media
+     raise Mastodon::ValidationError, I18n.t('media_attachments.validations.images_and_video') if @media.size > 1 && @media.find(&:video?)
    end
  
    def language_from_option(str)
index 447667c5a8bb9eca2e2e41373b8206fdf390c224,41d4714b904aad0a7c0a1d86bab1e99c05b0166f..9298ecbb048c3eb016ece9aa97f271cc5c54769a
        - else
          = link_to status.application.name, status.application.website, class: 'detailed-status__application', target: '_blank', rel: 'noopener'
        ·
-     = link_to remote_interaction_path(status), class: 'modal-button detailed-status__link' do
+     = link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do
 -      = fa_icon('reply')
 +      - if status.in_reply_to_id.nil?
 +        = fa_icon('reply')
 +      - else
 +        = fa_icon('reply-all')
        %span.detailed-status__reblogs>= number_to_human status.replies_count, strip_insignificant_zeros: true
        = " "
      ·
index 3a4dacc069bd70e0dfe96a10853e4bd6c42f22f0,89a6fe048161b8d93d5e91036d5cfaf2a40d7f53..1d44be7917f66f5258660244013b5e83489654aa
        %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }<
          %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}&nbsp;
          %a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
 -    .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
 +    .e-content{ lang: status.language, style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }<
 +      = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay)
  
-   - unless status.media_attachments.empty?
+   - if !status.media_attachments.empty?
      - if status.media_attachments.first.video?
        - video = status.media_attachments.first
        = react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description
  
    .status__action-bar
      .status__action-bar__counter
-       = link_to remote_interaction_path(status), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
+       = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
 -        = fa_icon 'reply fw'
 +        - if status.in_reply_to_id.nil?
 +          = fa_icon 'reply fw'
 +        - else
 +          = fa_icon 'reply-all fw'
        .status__action-bar__counter__label= obscured_counter status.replies_count
-     = link_to remote_interaction_path(status), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
+     = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do
        - if status.public_visibility? || status.unlisted_visibility?
          = fa_icon 'retweet fw'
        - elsif status.private_visibility?
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
index 31f735e192b54c16a676d209bc547698fce72731,91d1fdb8fe75a67c2c6721ffdf42e026a55c3c6d..d038d5a069a5d5010102c06927d0597dd1895c18
@@@ -824,8 -825,21 +825,20 @@@ sk
  
        <ul>
        <li><em>Základné informácie o účte</em>: Ak sa na tomto serveri zaregistruješ, budeš môcť byť požiadaný/á zadať prezývku, emailovú adresu a heslo. Budeš tiež môcť zadať aj ďalšie profilové údaje, ako napríklad meno a životopis, a nahrať profilovú fotku aj obrázok v záhlaví. Tvoja prezývka, meno, životopis, profilová fotka a obrázok v záhlaví sú vždy zobrazené verejne.</li><li><em>Príspevky, sledovania a iné verejné informácie</em>:
+       Zoznam ľudí, ktorých sleduješ je zobrazený verejne, a to isté platí aj pre zoznam tvojích následovateľov. Keď pošleš správu, ukladá sa jej dátum a čas, ale aj z akej aplikácie bola poslaná. Správy môžu obsahovať mediálne prílohy, ako obrázky a videá. Verejné, a nezaradené príspevky sú verejne prístupné. Keď si pripneš príspevok na svoj profil, toto je tiež verejne dostupnou informáciou. Tvoje príspevky sú takisto doručené tvojím sledovateľom, a to aj v rámci iných serverov, kde je potom uložená kópia tvojho príspevku. Ak vymažeš príspevok, táto akcia bude takisto doručená tvojím sledovateľom. Vyzdvihnutie, alebo obľúbenie iného príspevku je vždy verejne viditeľné.</li>
+       <li><em>Priame príspevky, a príspevky iba pre sledovateľov</em>: Všetky príspevky sú uložené a spracované na serveri. Príspevky iba pre sledovateľov sú doručené tvojím sledovateľom a užívateľom ktorí sú v nich spomenutí, pričom priame príspevky sú doručené iba tím užívateľom ktorí sú v nich spomenutí. V niektorých prípadoch to môže znamenať, že tieto príspevkz sú doručené aj vrámci iných serverov, a kópie príspevkov sú tam uložené.
+       V dobrej viere robíme všetko preto, aby bol prístup k tímto príspevkom vymedzený iba pre oprávnených používateľov, ale môže sa stať, že iné servery v tomto ohľade zlyhajú. Preto je dôležité prezrieť si a zhodnotiť, na aké servery patria tvoji následovatelia. V nastaveniach si môžeš zapnúť voľbu ručne povoľovať a odmietať nových následovateľov.
+       <em>Prosím maj na pamäti, že správcovia tvojho, aj vzdialeného obdŕžiavajúceho servera majú možnosť vidieť dané príspevky a správy, ale aj že obdŕžitelia týchto správ si ich môzu odfotiť, skopírovať, alebo ich inak zdieľať. <em>Nezdieľaj žiadne nebezpečné, alebo ohrozujúce správy pomocou Mastodonu!</em></li>
+        <li><em>IPky a iné metadáta</em>: Keď sa prihlásiš, zaznamenáva sa IP adresa z ktorej si sa prihlásil/a, takisto ako aj názov tvojho prehliadača. Všetky zaznamenané sezóny sú pre teba dostupné na konktolu, alebo na zamietnutie prístupu v nastaveniach. Posledná použitá IP adresa je uložená až po dobu dvanástich mesiacov. Môžeme si tiež ponechať serverové záznamy, ktoré obsahujú IP adresu každej požiadavky na tento server.</li>
+       </ul>
+       <hr class="spacer" />
+       <h3 id="use">
      title: Podmienky užívania, a pravidlá súkromia pre %{instance}
    themes:
 -    contrast: Vysoký kontrast
      default: Mastodon
      mastodon-light: Mastodon (svetlý)
    time:
Simple merge
Simple merge
index bd6578bd42740b8db2bd8a528081c6388e596472,4f7c2c8f32cc04348d89ec056a091b9b3eace7bf..4f070240adfc0c5f636e984f44960ad98662f788
@@@ -1,13 -1,8 +1,8 @@@
- # config/app.yml for rails-settings-cached
- #
- # This file contains default values, and does not need to be edited
- # when configuring an instance.  These settings may be changed by an
- # Administrator using the Web UI.
- #
- # For more information, see docs/Running-Mastodon/Administration-guide.md
- #
+ # This file contains default values, and does not need to be edited. All
+ # important settings can be changed from the admin interface.
  defaults: &defaults
 -  site_title: Mastodon
 +  site_title: 'dev.glitch.social'
    site_short_description: ''
    site_description: ''
    site_extended_description: ''
diff --cc db/schema.rb
Simple merge
Simple merge
diff --cc package.json
Simple merge
index a56158f1241f82a6c9c68147410cea4c41a549f7,c506cd87f171db43229ea4e2dec1fee8e0d1b6d9..df92094d19cedb51647c65874dcc6f263b5c316e
@@@ -119,13 -119,6 +119,13 @@@ RSpec.describe FeedManager d
          expect(FeedManager.instance.filter?(:home, status, bob.id)).to be true
        end
  
-         status = PostStatusService.new.call(alice, 'Hey @jeff')
 +      it 'returns true for status by followee mentioning muted account' do
 +        bob.mute!(jeff)
 +        bob.follow!(alice)
++        status = PostStatusService.new.call(alice, text: 'Hey @jeff')
 +        expect(FeedManager.instance.filter?(:home, status, bob.id)).to be true
 +      end
 +
        it 'returns true for reblog of a personally blocked domain' do
          alice.block_domain!('example.com')
          alice.follow!(jeff)
diff --cc yarn.lock
Simple merge