# frozen_string_literal: true
class AboutController < ApplicationController
+ before_action :set_pack
layout 'public'
- before_action :set_instance_presenter, only: [:show, :more, :terms]
+ before_action :set_body_classes, only: :show
+ before_action :set_instance_presenter
+ before_action :set_expires_in
- def show
- @hide_navbar = true
- end
+ skip_before_action :check_user_permissions, only: [:more, :terms]
- def more; end
+ def show; end
+
+ def more
+ flash.now[:notice] = I18n.t('about.instance_actor_flash') if params[:instance_actor]
+ end
def terms; end
def show
respond_to do |format|
format.html do
- mark_cacheable! unless user_signed_in?
+ use_pack 'public'
+ expires_in 0, public: true unless user_signed_in?
- @body_classes = 'with-modals'
@pinned_statuses = []
@endorsed_accounts = @account.endorsed_accounts.to_a.sample(4)
@trending_hashtags = TrendingTags.get(7)
@profile_directory = Setting.profile_directory
@timeline_preview = Setting.timeline_preview
+ @keybase_integration = Setting.enable_keybase
+ @spam_check_enabled = Setting.spam_check_enabled
end
private
def index
respond_to do |format|
format.html do
- mark_cacheable! unless user_signed_in?
+ use_pack 'public'
+ expires_in 0, public: true unless user_signed_in?
next if @account.user_hides_network?
end
def collection_presenter
- if params[:page].present?
+ options = { type: :ordered }
+ options[:size] = @account.followers_count unless Setting.hide_followers_count || @account.user&.setting_hide_followers_count
+ if page_requested?
ActivityPub::CollectionPresenter.new(
id: account_followers_url(@account, page: params.fetch(:page, 1)),
- type: :ordered,
- size: @account.followers_count,
items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) },
part_of: account_followers_url(@account),
next: page_url(follows.next_page),
def index
respond_to do |format|
format.html do
- mark_cacheable! unless user_signed_in?
+ use_pack 'public'
+ expires_in 0, public: true unless user_signed_in?
next if @account.user_hides_network?
# frozen_string_literal: true
class RemoteFollowController < ApplicationController
+ include AccountOwnedConcern
+
layout 'modal'
- before_action :set_account
+ before_action :set_pack
- before_action :gone, if: :suspended_account?
before_action :set_body_classes
def new
{ acct: session[:remote_follow] }
end
- def set_account
- @account = Account.find_local!(params[:account_username])
- end
-
- def suspended_account?
- @account.suspended?
- end
-
+ def set_pack
+ use_pack 'modal'
+ end
+
def set_body_classes
@body_classes = 'modal-layout'
@hide_header = true
def show
respond_to do |format|
format.html do
+ use_pack 'public'
+
expires_in 10.seconds, public: true if current_account.nil?
-
- @body_classes = 'with-modals'
-
set_ancestors
set_descendants
-
- render 'stream_entries/show'
end
format.json do
before_action :set_instance_presenter
def show
- @tag = Tag.find_normalized!(params[:id])
-
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
--- /dev/null
- if(target.getAttribute('data-autoplay') === 'false' && target.src !== swapSrc) {
+// This file will be loaded on public pages, regardless of theme.
+
+import createHistory from 'history/createBrowserHistory';
+import ready from '../mastodon/ready';
+
+const { delegate } = require('rails-ujs');
+const { length } = require('stringz');
+
+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, '.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');
+});
+
+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;
+});
if (parallaxComponents.length > 0 ) {
new Rellax('.parallax', { speed: -1 });
}
-
- if (document.body.classList.contains('with-modals')) {
- const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
- const scrollbarWidthStyle = document.createElement('style');
- scrollbarWidthStyle.id = 'scrollbar-width';
- document.head.appendChild(scrollbarWidthStyle);
- scrollbarWidthStyle.sheet.insertRule(`body.with-modals--active { margin-right: ${scrollbarWidth}px; }`, 0);
- }
});
-
- 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, '.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');
-
- 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;
- });
}
loadPolyfills().then(main).catch(error => {
include AccountCounters
include DomainNormalizable
+ MAX_DISPLAY_NAME_LENGTH = (ENV['MAX_DISPLAY_NAME_CHARS'] || 30).to_i
+ MAX_NOTE_LENGTH = (ENV['MAX_BIO_CHARS'] || 500).to_i
+ MAX_FIELDS = (ENV['MAX_PROFILE_FIELDS'] || 4).to_i
+
+ TRUST_LEVELS = {
+ untrusted: 0,
+ trusted: 1,
+ }.freeze
+
enum protocol: [:ostatus, :activitypub]
validates :username, presence: true
validates :username, format: { with: /\A#{USERNAME_RE}\z/i }, if: -> { !local? && will_save_change_to_username? }
# Local user validations
- validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? }
+ validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: 30 }, if: -> { local? && will_save_change_to_username? && actor_type != 'Application' }
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: 500 }, 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? }
+ 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) }
has_many :identity_proofs, class_name: 'AccountIdentityProof', dependent: :destroy, inverse_of: :account
# Timelines
- has_many :stream_entries, inverse_of: :account, dependent: :destroy
has_many :statuses, inverse_of: :account, dependent: :destroy
has_many :favourites, inverse_of: :account, dependent: :destroy
+ has_many :bookmarks, inverse_of: :account, dependent: :destroy
has_many :mentions, inverse_of: :account, dependent: :destroy
has_many :notifications, inverse_of: :account, dependent: :destroy
has_many :conversations, class_name: 'AccountConversation', dependent: :destroy, inverse_of: :account
thumbnail
hero
mascot
+ show_reblogs_in_public_timelines
+ show_replies_in_public_timelines
+ spam_check_enabled
).freeze
BOOLEAN_KEYS = %i(
show_known_fediverse_at_about_page
preview_sensitive_media
profile_directory
+ hide_followers_count
+ enable_keybase
+ show_reblogs_in_public_timelines
+ show_replies_in_public_timelines
+ spam_check_enabled
).freeze
UPLOAD_KEYS = %i(
has_many :session_activations, dependent: :destroy
- delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :delete_modal,
- :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network,
+ delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal,
+ :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count,
:expand_spoilers, :default_language, :aggregate_reblogs, :show_application,
- :advanced_layout, :default_content_type, :use_blurhash, :use_pending_items, to: :settings, prefix: :setting, allow_nil: false
- :advanced_layout, :use_blurhash, :use_pending_items, to: :settings, prefix: :setting, allow_nil: false
++ :advanced_layout, :default_content_type, :use_blurhash, :use_pending_items, :use_pending_items, to: :settings, prefix: :setting, allow_nil: false
attr_reader :invite_code
attr_writer :external
# Cannot be batched
statuses.each do |status|
unpush_from_public_timelines(status)
- batch_salmon_slaps(status) if status.local?
+ unpush_from_direct_timelines(status) if status.direct_visibility?
end
-
- Pubsubhubbub::RawDistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch }
- NotificationWorker.push_bulk(@salmon_batches) { |batch| batch }
end
private
end
end
end
-
- def batch_salmon_slaps(status)
- return if @mentions[status.id].empty?
-
- recipients = @mentions[status.id].map(&:account).reject(&:local?).select(&:ostatus?).uniq(&:domain).map(&:id)
-
- recipients.each do |recipient_id|
- @salmon_batches << [build_xml(status.stream_entry), status.account_id, recipient_id]
- end
- end
-
- def build_xml(stream_entry)
- return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id)
-
- @activity_xml[stream_entry.id] = stream_entry_to_xml(stream_entry)
- end
+
+ def unpush_from_direct_timelines(status)
+ payload = @json_payloads[status.id]
+ redis.pipelined do
+ @mentions[status.id].each do |mention|
+ FeedManager.instance.unpush_from_direct(mention.account, status) if mention.account.local?
+ end
+ FeedManager.instance.unpush_from_direct(status.account, status) if status.account.local?
+ end
+ end
end
def postprocess_status!
LinkCrawlWorker.perform_async(@status.id) unless @status.spoiler_text?
DistributionWorker.perform_async(@status.id)
-
- unless @status.local_only?
- Pubsubhubbub::DistributionWorker.perform_async(@status.stream_entry.id)
- ActivityPub::DistributionWorker.perform_async(@status.id)
- end
-
- ActivityPub::DistributionWorker.perform_async(@status.id)
++ ActivityPub::DistributionWorker.perform_async(@status.id) unless @status.local_only?
PollExpirationNotifyWorker.perform_at(@status.poll.expires_at, @status.poll.id) if @status.poll
end
if mentioned_account.local?
LocalNotificationWorker.perform_async(mentioned_account.id, mention.id, mention.class.name)
- elsif mentioned_account.ostatus? && !@status.stream_entry.hidden? && !@status.local_only?
- NotificationWorker.perform_async(ostatus_xml, @status.account_id, mentioned_account.id)
- elsif mentioned_account.activitypub?
+ elsif mentioned_account.activitypub? && !@status.local_only?
ActivityPub::DeliveryWorker.perform_async(activitypub_json, mention.status.account_id, mentioned_account.inbox_url)
end
end
reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility)
DistributionWorker.perform_async(reblog.id)
-
- unless reblogged_status.local_only?
- Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id)
- ActivityPub::DistributionWorker.perform_async(reblog.id)
- end
- ActivityPub::DistributionWorker.perform_async(reblog.id)
++ ActivityPub::DistributionWorker.perform_async(reblog.id) unless reblogged_status.local_only?
create_notification(reblog)
bump_potential_friendship(account, reblog)
remove_from_hashtags
remove_from_public
remove_from_media if status.media_attachments.any?
+ remove_from_direct if status.direct_visibility?
+ remove_from_spam_check
@status.destroy!
else
redis.publish('timeline:public:local:media', @payload) if @status.local?
end
+ def remove_from_direct
+ @mentions.each do |mention|
+ FeedManager.instance.unpush_from_direct(mention.account, @status) if mention.account.local?
+ end
+ end
+
+ def remove_from_spam_check
+ redis.zremrangebyscore("spam_check:#{@status.account_id}", @status.id, @status.id)
+ end
+
def lock_options
{ redis: Redis.current, key: "distribute:#{@status.id}" }
end
= feature_hint(link_to(t('admin.dashboard.feature_profile_directory'), edit_admin_settings_path), @profile_directory)
%li
= feature_hint(link_to(t('admin.dashboard.feature_timeline_preview'), edit_admin_settings_path), @timeline_preview)
+ %li
+ = feature_hint(link_to(t('admin.dashboard.keybase'), edit_admin_settings_path), @keybase_integration)
%li
= feature_hint(link_to(t('admin.dashboard.feature_relay'), admin_relays_path), @relay_enabled)
+ %li
+ = feature_hint(link_to(t('admin.dashboard.feature_spam_check'), edit_admin_settings_path), @spam_check_enabled)
.dashboard__widgets__versions
%div
.fields-group
= f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html')
+ .fields-group
+ = f.input :hide_followers_count, as: :boolean, wrapper: :with_label, label: t('admin.settings.hide_followers_count.title'), hint: t('admin.settings.hide_followers_count.desc_html')
+
+ .fields-group
+ = f.input :enable_keybase, as: :boolean, wrapper: :with_label, label: t('admin.settings.enable_keybase.title'), hint: t('admin.settings.enable_keybase.desc_html')
+
+ .fields-group
+ = f.input :show_reblogs_in_public_timelines, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_reblogs_in_public_timelines.title'), hint: t('admin.settings.show_reblogs_in_public_timelines.desc_html')
+
+ .fields-group
+ = f.input :show_replies_in_public_timelines, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_replies_in_public_timelines.title'), hint: t('admin.settings.show_replies_in_public_timelines.desc_html')
+
+ .fields-group
+ = f.input :spam_check_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.spam_check_enabled.title'), hint: t('admin.settings.spam_check_enabled.desc_html')
+
%hr.spacer/
.fields-group
setting_hide_network: Informacje o tym, kto Cię śledzi i kogo śledzisz nie będą widoczne
setting_noindex: Wpływa na widoczność strony profilu i Twoich wpisów
setting_show_application: W informacjach o wpisie będzie widoczna informacja o aplikacji, z której został wysłany
+ setting_skin: Zmienia wygląd używanej odmiany Mastodona
+ setting_use_blurhash: Gradienty są oparte na kolorach ukrywanej zawartości, ale uniewidaczniają wszystkie szczegóły
username: Twoja nazwa użytkownika będzie niepowtarzalna na %{domain}
whole_word: Jeśli słowo lub fraza składa się jedynie z liter lub cyfr, filtr będzie zastosowany tylko do pełnych wystąpień
featured_tag:
setting_noindex: Nie indeksuj mojego profilu w wyszukiwarkach internetowych
setting_reduce_motion: Ogranicz ruch w animacjach
setting_show_application: Informuj o aplikacji z której wysłano wpisy
+ setting_skin: Motyw
setting_system_font_ui: Używaj domyślnej czcionki systemu
- setting_theme: Motyw strony
setting_unfollow_modal: Pytaj o potwierdzenie przed cofnięciem śledzenia
+ setting_use_blurhash: Pokazuj kolorowe gradienty dla ukrytej zawartości multimedialnej
severity: Priorytet
type: Importowane dane
username: Nazwa użytkownika
get '/search', to: 'search#index', as: :search
- resources :follows, only: [:create]
resources :media, only: [:create, :update]
resources :blocks, only: [:index]
- resources :mutes, only: [:index]
+ resources :mutes, only: [:index] do
+ collection do
+ get 'details'
+ end
+ end
resources :favourites, only: [:index]
+ resources :bookmarks, only: [:index]
resources :reports, only: [:create]
resources :filters, only: [:index, :create, :show, :update, :destroy]
resources :endorsements, only: [:index]
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
development:
<<: *defaults
describe '::MASTODON_STRICT' do
subject { Sanitize::Config::MASTODON_STRICT }
- it 'converts h1 to p' do
- expect(Sanitize.fragment('<h1>Foo</h1>', subject)).to eq '<p>Foo</p>'
+ it 'keeps h1' do
+ expect(Sanitize.fragment('<h1>Foo</h1>', subject)).to eq '<h1>Foo</h1>'
end
- it 'converts ul to p' do
- expect(Sanitize.fragment('<p>Check out:</p><ul><li>Foo</li><li>Bar</li></ul>', subject)).to eq '<p>Check out:</p><p>Foo<br>Bar</p>'
- end
-
- it 'converts p inside ul' do
- expect(Sanitize.fragment('<ul><li><p>Foo</p><p>Bar</p></li><li>Baz</li></ul>', subject)).to eq '<p>Foo<br>Bar<br>Baz</p>'
- end
-
- it 'converts ul inside ul' do
- expect(Sanitize.fragment('<ul><li>Foo</li><li><ul><li>Bar</li><li>Baz</li></ul></li></ul>', subject)).to eq '<p>Foo<br>Bar<br>Baz</p>'
+ it 'keeps ul' do
+ expect(Sanitize.fragment('<p>Check out:</p><ul><li>Foo</li><li>Bar</li></ul>', subject)).to eq '<p>Check out:</p><ul><li>Foo</li><li>Bar</li></ul>'
end
+
+ it 'keep links in lists' do
+ expect(Sanitize.fragment('<p>Check out:</p><ul><li><a href="https://joinmastodon.org" rel="nofollow noopener" target="_blank">joinmastodon.org</a></li><li>Bar</li></ul>', subject)).to eq '<p>Check out:</p><p><a href="https://joinmastodon.org" rel="nofollow noopener" target="_blank">joinmastodon.org</a><br>Bar</p>'
+ end
end
end