Merge pull request #2843 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to 0226bbe516
			
			
This commit is contained in:
		
						commit
						065abf2918
					
				| 
						 | 
					@ -333,7 +333,7 @@ module.exports = defineConfig({
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      parserOptions: {
 | 
					      parserOptions: {
 | 
				
			||||||
        project: true,
 | 
					        projectService: true,
 | 
				
			||||||
        tsconfigRootDir: __dirname,
 | 
					        tsconfigRootDir: __dirname,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										16
									
								
								Gemfile.lock
								
								
								
								
							
							
						
						
									
										16
									
								
								Gemfile.lock
								
								
								
								
							| 
						 | 
					@ -100,17 +100,17 @@ GEM
 | 
				
			||||||
    attr_required (1.0.2)
 | 
					    attr_required (1.0.2)
 | 
				
			||||||
    awrence (1.2.1)
 | 
					    awrence (1.2.1)
 | 
				
			||||||
    aws-eventstream (1.3.0)
 | 
					    aws-eventstream (1.3.0)
 | 
				
			||||||
    aws-partitions (1.970.0)
 | 
					    aws-partitions (1.974.0)
 | 
				
			||||||
    aws-sdk-core (3.203.0)
 | 
					    aws-sdk-core (3.205.0)
 | 
				
			||||||
      aws-eventstream (~> 1, >= 1.3.0)
 | 
					      aws-eventstream (~> 1, >= 1.3.0)
 | 
				
			||||||
      aws-partitions (~> 1, >= 1.651.0)
 | 
					      aws-partitions (~> 1, >= 1.651.0)
 | 
				
			||||||
      aws-sigv4 (~> 1.9)
 | 
					      aws-sigv4 (~> 1.9)
 | 
				
			||||||
      jmespath (~> 1, >= 1.6.1)
 | 
					      jmespath (~> 1, >= 1.6.1)
 | 
				
			||||||
    aws-sdk-kms (1.89.0)
 | 
					    aws-sdk-kms (1.91.0)
 | 
				
			||||||
      aws-sdk-core (~> 3, >= 3.203.0)
 | 
					      aws-sdk-core (~> 3, >= 3.205.0)
 | 
				
			||||||
      aws-sigv4 (~> 1.5)
 | 
					      aws-sigv4 (~> 1.5)
 | 
				
			||||||
    aws-sdk-s3 (1.160.0)
 | 
					    aws-sdk-s3 (1.162.0)
 | 
				
			||||||
      aws-sdk-core (~> 3, >= 3.203.0)
 | 
					      aws-sdk-core (~> 3, >= 3.205.0)
 | 
				
			||||||
      aws-sdk-kms (~> 1)
 | 
					      aws-sdk-kms (~> 1)
 | 
				
			||||||
      aws-sigv4 (~> 1.5)
 | 
					      aws-sigv4 (~> 1.5)
 | 
				
			||||||
    aws-sigv4 (1.9.1)
 | 
					    aws-sigv4 (1.9.1)
 | 
				
			||||||
| 
						 | 
					@ -601,7 +601,7 @@ GEM
 | 
				
			||||||
      actionmailer (>= 3)
 | 
					      actionmailer (>= 3)
 | 
				
			||||||
      net-smtp
 | 
					      net-smtp
 | 
				
			||||||
      premailer (~> 1.7, >= 1.7.9)
 | 
					      premailer (~> 1.7, >= 1.7.9)
 | 
				
			||||||
    propshaft (0.9.1)
 | 
					    propshaft (1.0.0)
 | 
				
			||||||
      actionpack (>= 7.0.0)
 | 
					      actionpack (>= 7.0.0)
 | 
				
			||||||
      activesupport (>= 7.0.0)
 | 
					      activesupport (>= 7.0.0)
 | 
				
			||||||
      rack
 | 
					      rack
 | 
				
			||||||
| 
						 | 
					@ -691,7 +691,7 @@ GEM
 | 
				
			||||||
    redlock (1.3.2)
 | 
					    redlock (1.3.2)
 | 
				
			||||||
      redis (>= 3.0.0, < 6.0)
 | 
					      redis (>= 3.0.0, < 6.0)
 | 
				
			||||||
    regexp_parser (2.9.2)
 | 
					    regexp_parser (2.9.2)
 | 
				
			||||||
    reline (0.5.9)
 | 
					    reline (0.5.10)
 | 
				
			||||||
      io-console (~> 0.5)
 | 
					      io-console (~> 0.5)
 | 
				
			||||||
    request_store (1.6.0)
 | 
					    request_store (1.6.0)
 | 
				
			||||||
      rack (>= 1.4)
 | 
					      rack (>= 1.4)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,6 +8,16 @@ module WebAppControllerConcern
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    before_action :redirect_unauthenticated_to_permalinks!
 | 
					    before_action :redirect_unauthenticated_to_permalinks!
 | 
				
			||||||
    before_action :set_app_body_class
 | 
					    before_action :set_app_body_class
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    content_security_policy do |p|
 | 
				
			||||||
 | 
					      policy = ContentSecurityPolicy.new
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if policy.sso_host.present?
 | 
				
			||||||
 | 
					        p.form_action policy.sso_host
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        p.form_action :none
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def skip_csrf_meta_tags?
 | 
					  def skip_csrf_meta_tags?
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,6 @@ class Redirect::BaseController < ApplicationController
 | 
				
			||||||
  vary_by 'Accept-Language'
 | 
					  vary_by 'Accept-Language'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  before_action :set_resource
 | 
					  before_action :set_resource
 | 
				
			||||||
  before_action :set_app_body_class
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def show
 | 
					  def show
 | 
				
			||||||
    @redirect_path = ActivityPub::TagManager.instance.url_for(@resource)
 | 
					    @redirect_path = ActivityPub::TagManager.instance.url_for(@resource)
 | 
				
			||||||
| 
						 | 
					@ -14,10 +13,6 @@ class Redirect::BaseController < ApplicationController
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_app_body_class
 | 
					 | 
				
			||||||
    @body_classes = 'app-body'
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def set_resource
 | 
					  def set_resource
 | 
				
			||||||
    raise NotImplementedError
 | 
					    raise NotImplementedError
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,6 @@ class StatusesController < ApplicationController
 | 
				
			||||||
  before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
 | 
					  before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
 | 
				
			||||||
  before_action :set_status
 | 
					  before_action :set_status
 | 
				
			||||||
  before_action :redirect_to_original, only: :show
 | 
					  before_action :redirect_to_original, only: :show
 | 
				
			||||||
  before_action :set_body_classes, only: :embed
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  after_action :set_link_headers
 | 
					  after_action :set_link_headers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,10 +50,6 @@ class StatusesController < ApplicationController
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_body_classes
 | 
					 | 
				
			||||||
    @body_classes = 'with-modals'
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def set_link_headers
 | 
					  def set_link_headers
 | 
				
			||||||
    response.headers['Link'] = LinkHeader.new(
 | 
					    response.headers['Link'] = LinkHeader.new(
 | 
				
			||||||
      [[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]
 | 
					      [[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,14 +19,6 @@ module AccountsHelper
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def account_action_button(account)
 | 
					 | 
				
			||||||
    return if account.memorial? || account.moved?
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do
 | 
					 | 
				
			||||||
      safe_join([logo_as_symbol, t('accounts.follow')])
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def hide_followers_count?(account)
 | 
					  def hide_followers_count?(account)
 | 
				
			||||||
    Setting.hide_followers_count || account.user&.settings&.[]('hide_followers_count')
 | 
					    Setting.hide_followers_count || account.user&.settings&.[]('hide_followers_count')
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,26 +57,6 @@ module MediaComponentHelper
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def render_card_component(status, **options)
 | 
					 | 
				
			||||||
    component_params = {
 | 
					 | 
				
			||||||
      sensitive: sensitive_viewer?(status, current_account),
 | 
					 | 
				
			||||||
      card: serialize_status_card(status).as_json,
 | 
					 | 
				
			||||||
    }.merge(**options)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    react_component :card, component_params
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def render_poll_component(status, **options)
 | 
					 | 
				
			||||||
    component_params = {
 | 
					 | 
				
			||||||
      disabled: true,
 | 
					 | 
				
			||||||
      poll: serialize_status_poll(status).as_json,
 | 
					 | 
				
			||||||
    }.merge(**options)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    react_component :poll, component_params do
 | 
					 | 
				
			||||||
      render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? }
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def serialize_media_attachment(attachment)
 | 
					  def serialize_media_attachment(attachment)
 | 
				
			||||||
| 
						 | 
					@ -86,22 +66,6 @@ module MediaComponentHelper
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def serialize_status_card(status)
 | 
					 | 
				
			||||||
    ActiveModelSerializers::SerializableResource.new(
 | 
					 | 
				
			||||||
      status.preview_card,
 | 
					 | 
				
			||||||
      serializer: REST::PreviewCardSerializer
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def serialize_status_poll(status)
 | 
					 | 
				
			||||||
    ActiveModelSerializers::SerializableResource.new(
 | 
					 | 
				
			||||||
      status.preloadable_poll,
 | 
					 | 
				
			||||||
      serializer: REST::PollSerializer,
 | 
					 | 
				
			||||||
      scope: current_user,
 | 
					 | 
				
			||||||
      scope_name: :current_user
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def sensitive_viewer?(status, account)
 | 
					  def sensitive_viewer?(status, account)
 | 
				
			||||||
    if !account.nil? && account.id == status.account_id
 | 
					    if !account.nil? && account.id == status.account_id
 | 
				
			||||||
      status.sensitive
 | 
					      status.sensitive
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,13 @@ module StatusesHelper
 | 
				
			||||||
  EMBEDDED_CONTROLLER = 'statuses'
 | 
					  EMBEDDED_CONTROLLER = 'statuses'
 | 
				
			||||||
  EMBEDDED_ACTION = 'embed'
 | 
					  EMBEDDED_ACTION = 'embed'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  VISIBLITY_ICONS = {
 | 
				
			||||||
 | 
					    public: 'globe',
 | 
				
			||||||
 | 
					    unlisted: 'lock_open',
 | 
				
			||||||
 | 
					    private: 'lock',
 | 
				
			||||||
 | 
					    direct: 'alternate_email',
 | 
				
			||||||
 | 
					  }.freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def nothing_here(extra_classes = '')
 | 
					  def nothing_here(extra_classes = '')
 | 
				
			||||||
    content_tag(:div, class: "nothing-here #{extra_classes}") do
 | 
					    content_tag(:div, class: "nothing-here #{extra_classes}") do
 | 
				
			||||||
      t('accounts.nothing_here')
 | 
					      t('accounts.nothing_here')
 | 
				
			||||||
| 
						 | 
					@ -57,17 +64,8 @@ module StatusesHelper
 | 
				
			||||||
    embedded_view? ? '_blank' : nil
 | 
					    embedded_view? ? '_blank' : nil
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def fa_visibility_icon(status)
 | 
					  def visibility_icon(status)
 | 
				
			||||||
    case status.visibility
 | 
					    VISIBLITY_ICONS[status.visibility.to_sym]
 | 
				
			||||||
    when 'public'
 | 
					 | 
				
			||||||
      material_symbol 'globe'
 | 
					 | 
				
			||||||
    when 'unlisted'
 | 
					 | 
				
			||||||
      material_symbol 'lock_open'
 | 
					 | 
				
			||||||
    when 'private'
 | 
					 | 
				
			||||||
      material_symbol 'lock'
 | 
					 | 
				
			||||||
    when 'direct'
 | 
					 | 
				
			||||||
      material_symbol 'alternate_email'
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def embedded_view?
 | 
					  def embedded_view?
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,74 @@
 | 
				
			||||||
 | 
					import './public-path';
 | 
				
			||||||
 | 
					import { createRoot } from 'react-dom/client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { start } from '../mastodon/common';
 | 
				
			||||||
 | 
					import { Status } from '../mastodon/features/standalone/status';
 | 
				
			||||||
 | 
					import { loadPolyfills } from '../mastodon/polyfills';
 | 
				
			||||||
 | 
					import ready from '../mastodon/ready';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					start();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function loaded() {
 | 
				
			||||||
 | 
					  const mountNode = document.getElementById('mastodon-status');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (mountNode) {
 | 
				
			||||||
 | 
					    const attr = mountNode.getAttribute('data-props');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!attr) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const props = JSON.parse(attr) as { id: string; locale: string };
 | 
				
			||||||
 | 
					    const root = createRoot(mountNode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    root.render(<Status {...props} />);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function main() {
 | 
				
			||||||
 | 
					  ready(loaded).catch((error: unknown) => {
 | 
				
			||||||
 | 
					    console.error(error);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					loadPolyfills()
 | 
				
			||||||
 | 
					  .then(main)
 | 
				
			||||||
 | 
					  .catch((error: unknown) => {
 | 
				
			||||||
 | 
					    console.error(error);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SetHeightMessage {
 | 
				
			||||||
 | 
					  type: 'setHeight';
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  height: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isSetHeightMessage(data: unknown): data is SetHeightMessage {
 | 
				
			||||||
 | 
					  if (
 | 
				
			||||||
 | 
					    data &&
 | 
				
			||||||
 | 
					    typeof data === 'object' &&
 | 
				
			||||||
 | 
					    'type' in data &&
 | 
				
			||||||
 | 
					    data.type === 'setHeight'
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  else return false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					window.addEventListener('message', (e) => {
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
 | 
				
			||||||
 | 
					  if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const data = e.data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // We use a timeout to allow for the React page to render before calculating the height
 | 
				
			||||||
 | 
					  afterInitialRender(() => {
 | 
				
			||||||
 | 
					    window.parent.postMessage(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        type: 'setHeight',
 | 
				
			||||||
 | 
					        id: data.id,
 | 
				
			||||||
 | 
					        height: document.getElementsByTagName('html')[0]?.scrollHeight,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      '*',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -37,43 +37,6 @@ const messages = defineMessages({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface SetHeightMessage {
 | 
					 | 
				
			||||||
  type: 'setHeight';
 | 
					 | 
				
			||||||
  id: string;
 | 
					 | 
				
			||||||
  height: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function isSetHeightMessage(data: unknown): data is SetHeightMessage {
 | 
					 | 
				
			||||||
  if (
 | 
					 | 
				
			||||||
    data &&
 | 
					 | 
				
			||||||
    typeof data === 'object' &&
 | 
					 | 
				
			||||||
    'type' in data &&
 | 
					 | 
				
			||||||
    data.type === 'setHeight'
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
  else return false;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
window.addEventListener('message', (e) => {
 | 
					 | 
				
			||||||
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
 | 
					 | 
				
			||||||
  if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const data = e.data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ready(() => {
 | 
					 | 
				
			||||||
    window.parent.postMessage(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        type: 'setHeight',
 | 
					 | 
				
			||||||
        id: data.id,
 | 
					 | 
				
			||||||
        height: document.getElementsByTagName('html')[0]?.scrollHeight,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      '*',
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }).catch((e: unknown) => {
 | 
					 | 
				
			||||||
    console.error('Error in setHeightMessage postMessage', e);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function loaded() {
 | 
					function loaded() {
 | 
				
			||||||
  const { messages: localeData } = getLocale();
 | 
					  const { messages: localeData } = getLocale();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -65,7 +65,7 @@ export const synchronouslySubmitMarkers = createAppAsyncThunk(
 | 
				
			||||||
      client.setRequestHeader('Content-Type', 'application/json');
 | 
					      client.setRequestHeader('Content-Type', 'application/json');
 | 
				
			||||||
      client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
 | 
					      client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
 | 
				
			||||||
      client.send(JSON.stringify(params));
 | 
					      client.send(JSON.stringify(params));
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch {
 | 
				
			||||||
      // Do not make the BeforeUnload handler error out
 | 
					      // Do not make the BeforeUnload handler error out
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,11 +49,13 @@ export function fetchStatusRequest(id, skipLoading) {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchStatus(id, forceFetch = false) {
 | 
					export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
 | 
				
			||||||
  return (dispatch, getState) => {
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
    const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
 | 
					    const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (alsoFetchContext) {
 | 
				
			||||||
      dispatch(fetchContext(id));
 | 
					      dispatch(fetchContext(id));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (skipLoading) {
 | 
					    if (skipLoading) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -151,7 +151,7 @@ async function refreshHomeTimelineAndNotification(dispatch, getState) {
 | 
				
			||||||
    // TODO: polling for merged notifications
 | 
					    // TODO: polling for merged notifications
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await dispatch(pollRecentGroupNotifications());
 | 
					      await dispatch(pollRecentGroupNotifications());
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ export function start() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    Rails.start();
 | 
					    Rails.start();
 | 
				
			||||||
  } catch (e) {
 | 
					  } catch {
 | 
				
			||||||
    // If called twice
 | 
					    // If called twice
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,90 @@
 | 
				
			||||||
 | 
					import { useRef, useState, useCallback } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
 | 
				
			||||||
 | 
					import { Icon } from 'flavours/glitch/components/icon';
 | 
				
			||||||
 | 
					import { useTimeout } from 'flavours/glitch/hooks/useTimeout';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
 | 
				
			||||||
 | 
					  const inputRef = useRef<HTMLTextAreaElement>(null);
 | 
				
			||||||
 | 
					  const [copied, setCopied] = useState(false);
 | 
				
			||||||
 | 
					  const [focused, setFocused] = useState(false);
 | 
				
			||||||
 | 
					  const [setAnimationTimeout] = useTimeout();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleInputClick = useCallback(() => {
 | 
				
			||||||
 | 
					    setCopied(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (inputRef.current) {
 | 
				
			||||||
 | 
					      inputRef.current.focus();
 | 
				
			||||||
 | 
					      inputRef.current.select();
 | 
				
			||||||
 | 
					      inputRef.current.setSelectionRange(0, value.length);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [setCopied, value]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleButtonClick = useCallback(
 | 
				
			||||||
 | 
					    (e: React.MouseEvent) => {
 | 
				
			||||||
 | 
					      e.stopPropagation();
 | 
				
			||||||
 | 
					      void navigator.clipboard.writeText(value);
 | 
				
			||||||
 | 
					      inputRef.current?.blur();
 | 
				
			||||||
 | 
					      setCopied(true);
 | 
				
			||||||
 | 
					      setAnimationTimeout(() => {
 | 
				
			||||||
 | 
					        setCopied(false);
 | 
				
			||||||
 | 
					      }, 700);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [setCopied, setAnimationTimeout, value],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleKeyUp = useCallback(
 | 
				
			||||||
 | 
					    (e: React.KeyboardEvent) => {
 | 
				
			||||||
 | 
					      if (e.key !== ' ') return;
 | 
				
			||||||
 | 
					      void navigator.clipboard.writeText(value);
 | 
				
			||||||
 | 
					      setCopied(true);
 | 
				
			||||||
 | 
					      setAnimationTimeout(() => {
 | 
				
			||||||
 | 
					        setCopied(false);
 | 
				
			||||||
 | 
					      }, 700);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [setCopied, setAnimationTimeout, value],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleFocus = useCallback(() => {
 | 
				
			||||||
 | 
					    setFocused(true);
 | 
				
			||||||
 | 
					  }, [setFocused]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleBlur = useCallback(() => {
 | 
				
			||||||
 | 
					    setFocused(false);
 | 
				
			||||||
 | 
					  }, [setFocused]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={classNames('copy-paste-text', { copied, focused })}
 | 
				
			||||||
 | 
					      tabIndex={0}
 | 
				
			||||||
 | 
					      role='button'
 | 
				
			||||||
 | 
					      onClick={handleInputClick}
 | 
				
			||||||
 | 
					      onKeyUp={handleKeyUp}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <textarea
 | 
				
			||||||
 | 
					        readOnly
 | 
				
			||||||
 | 
					        value={value}
 | 
				
			||||||
 | 
					        ref={inputRef}
 | 
				
			||||||
 | 
					        onClick={handleInputClick}
 | 
				
			||||||
 | 
					        onFocus={handleFocus}
 | 
				
			||||||
 | 
					        onBlur={handleBlur}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <button className='button' onClick={handleButtonClick}>
 | 
				
			||||||
 | 
					        <Icon id='copy' icon={ContentCopyIcon} />{' '}
 | 
				
			||||||
 | 
					        {copied ? (
 | 
				
			||||||
 | 
					          <FormattedMessage id='copypaste.copied' defaultMessage='Copied' />
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          <FormattedMessage
 | 
				
			||||||
 | 
					            id='copypaste.copy_to_clipboard'
 | 
				
			||||||
 | 
					            defaultMessage='Copy to clipboard'
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -60,8 +60,8 @@ export default class ErrorBoundary extends PureComponent {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      textarea.select();
 | 
					      textarea.select();
 | 
				
			||||||
      document.execCommand('copy');
 | 
					      document.execCommand('copy');
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch {
 | 
				
			||||||
 | 
					      // do nothing
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      document.body.removeChild(textarea);
 | 
					      document.body.removeChild(textarea);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,13 @@ export const WordmarkLogo: React.FC = () => (
 | 
				
			||||||
  </svg>
 | 
					  </svg>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const IconLogo: React.FC = () => (
 | 
				
			||||||
 | 
					  <svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
 | 
				
			||||||
 | 
					    <title>Mastodon</title>
 | 
				
			||||||
 | 
					    <use xlinkHref='#logo-symbol-icon' />
 | 
				
			||||||
 | 
					  </svg>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SymbolLogo: React.FC = () => (
 | 
					export const SymbolLogo: React.FC = () => (
 | 
				
			||||||
  <img src={logo} alt='Mastodon' className='logo logo--icon' />
 | 
					  <img src={logo} alt='Mastodon' className='logo logo--icon' />
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -312,7 +312,7 @@ class MediaGallery extends PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const style = {};
 | 
					    const style = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const computedClass = classNames('media-gallery', { 'full-width': fullwidth });
 | 
					    const computedClass = classNames('media-gallery', `media-gallery--layout-${size}`, { 'full-width': fullwidth });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.isStandaloneEligible()) { // TODO: cropImages setting
 | 
					    if (this.isStandaloneEligible()) { // TODO: cropImages setting
 | 
				
			||||||
      style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`;
 | 
					      style.aspectRatio = `${this.props.media.getIn([0, 'meta', 'small', 'aspect'])}`;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,14 +2,12 @@ import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { IconLogo } from 'flavours/glitch/components/logo';
 | 
				
			||||||
import { AuthorLink } from 'flavours/glitch/features/explore/components/author_link';
 | 
					import { AuthorLink } from 'flavours/glitch/features/explore/components/author_link';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const MoreFromAuthor = ({ accountId }) => (
 | 
					export const MoreFromAuthor = ({ accountId }) => (
 | 
				
			||||||
  <div className='more-from-author'>
 | 
					  <div className='more-from-author'>
 | 
				
			||||||
    <svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
 | 
					    <IconLogo />
 | 
				
			||||||
      <use xlinkHref='#logo-symbol-icon' />
 | 
					 | 
				
			||||||
    </svg>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
 | 
					    <FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,7 +56,7 @@ const messages = defineMessages({
 | 
				
			||||||
  unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
 | 
					  unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
 | 
				
			||||||
  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
 | 
					  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
 | 
				
			||||||
  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
 | 
					  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
 | 
				
			||||||
  embed: { id: 'status.embed', defaultMessage: 'Embed' },
 | 
					  embed: { id: 'status.embed', defaultMessage: 'Get embed code' },
 | 
				
			||||||
  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
 | 
					  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
 | 
				
			||||||
  admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
 | 
					  admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
 | 
				
			||||||
  admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
 | 
					  admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,8 +34,6 @@ import Status from 'flavours/glitch/components/status';
 | 
				
			||||||
import { deleteModal } from 'flavours/glitch/initial_state';
 | 
					import { deleteModal } from 'flavours/glitch/initial_state';
 | 
				
			||||||
import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
 | 
					import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { showAlertForError } from '../actions/alerts';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const makeMapStateToProps = () => {
 | 
					const makeMapStateToProps = () => {
 | 
				
			||||||
  const getStatus = makeGetStatus();
 | 
					  const getStatus = makeGetStatus();
 | 
				
			||||||
  const getPictureInPicture = makeGetPictureInPicture();
 | 
					  const getPictureInPicture = makeGetPictureInPicture();
 | 
				
			||||||
| 
						 | 
					@ -111,10 +109,7 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
 | 
				
			||||||
  onEmbed (status) {
 | 
					  onEmbed (status) {
 | 
				
			||||||
    dispatch(openModal({
 | 
					    dispatch(openModal({
 | 
				
			||||||
      modalType: 'EMBED',
 | 
					      modalType: 'EMBED',
 | 
				
			||||||
      modalProps: {
 | 
					      modalProps: { id: status.get('id') },
 | 
				
			||||||
        id: status.get('id'),
 | 
					 | 
				
			||||||
        onError: error => dispatch(showAlertForError(error)),
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    }));
 | 
					    }));
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,74 @@
 | 
				
			||||||
 | 
					import { createRoot } from 'react-dom/client';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import '@/entrypoints/public-path';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { start } from 'flavours/glitch/common';
 | 
				
			||||||
 | 
					import { Status } from 'flavours/glitch/features/standalone/status';
 | 
				
			||||||
 | 
					import { afterInitialRender } from 'flavours/glitch/hooks/useRenderSignal';
 | 
				
			||||||
 | 
					import { loadPolyfills } from 'flavours/glitch/polyfills';
 | 
				
			||||||
 | 
					import ready from 'flavours/glitch/ready';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					start();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function loaded() {
 | 
				
			||||||
 | 
					  const mountNode = document.getElementById('mastodon-status');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (mountNode) {
 | 
				
			||||||
 | 
					    const attr = mountNode.getAttribute('data-props');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!attr) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const props = JSON.parse(attr) as { id: string; locale: string };
 | 
				
			||||||
 | 
					    const root = createRoot(mountNode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    root.render(<Status {...props} />);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function main() {
 | 
				
			||||||
 | 
					  ready(loaded).catch((error: unknown) => {
 | 
				
			||||||
 | 
					    console.error(error);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					loadPolyfills()
 | 
				
			||||||
 | 
					  .then(main)
 | 
				
			||||||
 | 
					  .catch((error: unknown) => {
 | 
				
			||||||
 | 
					    console.error(error);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SetHeightMessage {
 | 
				
			||||||
 | 
					  type: 'setHeight';
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  height: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function isSetHeightMessage(data: unknown): data is SetHeightMessage {
 | 
				
			||||||
 | 
					  if (
 | 
				
			||||||
 | 
					    data &&
 | 
				
			||||||
 | 
					    typeof data === 'object' &&
 | 
				
			||||||
 | 
					    'type' in data &&
 | 
				
			||||||
 | 
					    data.type === 'setHeight'
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  else return false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					window.addEventListener('message', (e) => {
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
 | 
				
			||||||
 | 
					  if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const data = e.data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // We use a timeout to allow for the React page to render before calculating the height
 | 
				
			||||||
 | 
					  afterInitialRender(() => {
 | 
				
			||||||
 | 
					    window.parent.postMessage(
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        type: 'setHeight',
 | 
				
			||||||
 | 
					        id: data.id,
 | 
				
			||||||
 | 
					        height: document.getElementsByTagName('html')[0]?.scrollHeight,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      '*',
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -37,43 +37,6 @@ const messages = defineMessages({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface SetHeightMessage {
 | 
					 | 
				
			||||||
  type: 'setHeight';
 | 
					 | 
				
			||||||
  id: string;
 | 
					 | 
				
			||||||
  height: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function isSetHeightMessage(data: unknown): data is SetHeightMessage {
 | 
					 | 
				
			||||||
  if (
 | 
					 | 
				
			||||||
    data &&
 | 
					 | 
				
			||||||
    typeof data === 'object' &&
 | 
					 | 
				
			||||||
    'type' in data &&
 | 
					 | 
				
			||||||
    data.type === 'setHeight'
 | 
					 | 
				
			||||||
  )
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
  else return false;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
window.addEventListener('message', (e) => {
 | 
					 | 
				
			||||||
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
 | 
					 | 
				
			||||||
  if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const data = e.data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ready(() => {
 | 
					 | 
				
			||||||
    window.parent.postMessage(
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        type: 'setHeight',
 | 
					 | 
				
			||||||
        id: data.id,
 | 
					 | 
				
			||||||
        height: document.getElementsByTagName('html')[0]?.scrollHeight,
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      '*',
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }).catch((e: unknown) => {
 | 
					 | 
				
			||||||
    console.error('Error in setHeightMessage postMessage', e);
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function loaded() {
 | 
					function loaded() {
 | 
				
			||||||
  const { messages: localeData } = getLocale();
 | 
					  const { messages: localeData } = getLocale();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -131,7 +131,7 @@ class LoginForm extends React.PureComponent {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      new URL(url);
 | 
					      new URL(url);
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    } catch(_) {
 | 
					    } catch {
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,7 @@
 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { isEqual } from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
 | 
					import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
 | 
				
			||||||
import ReplyIcon from '@/material-icons/400-24px/reply-fill.svg?react';
 | 
					import ReplyIcon from '@/material-icons/400-24px/reply-fill.svg?react';
 | 
				
			||||||
import { me } from 'flavours/glitch/initial_state';
 | 
					import { me } from 'flavours/glitch/initial_state';
 | 
				
			||||||
| 
						 | 
					@ -47,7 +49,7 @@ export const NotificationMention: React.FC<{
 | 
				
			||||||
      status.get('visibility') === 'direct',
 | 
					      status.get('visibility') === 'direct',
 | 
				
			||||||
      status.get('in_reply_to_account_id') === me,
 | 
					      status.get('in_reply_to_account_id') === me,
 | 
				
			||||||
    ] as const;
 | 
					    ] as const;
 | 
				
			||||||
  });
 | 
					  }, isEqual);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let labelRenderer = mentionLabelRenderer;
 | 
					  let labelRenderer = mentionLabelRenderer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Helmet } from 'react-helmet';
 | 
					import { Helmet } from 'react-helmet';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { isEqual } from 'lodash';
 | 
				
			||||||
import { useDebouncedCallback } from 'use-debounce';
 | 
					import { useDebouncedCallback } from 'use-debounce';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
 | 
					import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
 | 
				
			||||||
| 
						 | 
					@ -62,7 +63,7 @@ export const Notifications: React.FC<{
 | 
				
			||||||
  multiColumn?: boolean;
 | 
					  multiColumn?: boolean;
 | 
				
			||||||
}> = ({ columnId, multiColumn }) => {
 | 
					}> = ({ columnId, multiColumn }) => {
 | 
				
			||||||
  const intl = useIntl();
 | 
					  const intl = useIntl();
 | 
				
			||||||
  const notifications = useAppSelector(selectNotificationGroups);
 | 
					  const notifications = useAppSelector(selectNotificationGroups, isEqual);
 | 
				
			||||||
  const dispatch = useAppDispatch();
 | 
					  const dispatch = useAppDispatch();
 | 
				
			||||||
  const isLoading = useAppSelector((s) => s.notificationGroups.isLoading);
 | 
					  const isLoading = useAppSelector((s) => s.notificationGroups.isLoading);
 | 
				
			||||||
  const hasMore = notifications.at(-1)?.type === 'gap';
 | 
					  const hasMore = notifications.at(-1)?.type === 'gap';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,8 +10,8 @@ import { Link } from 'react-router-dom';
 | 
				
			||||||
import SwipeableViews from 'react-swipeable-views';
 | 
					import SwipeableViews from 'react-swipeable-views';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
 | 
					import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
 | 
				
			||||||
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
 | 
					 | 
				
			||||||
import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
 | 
					import { ColumnBackButton } from 'flavours/glitch/components/column_back_button';
 | 
				
			||||||
 | 
					import { CopyPasteText } from 'flavours/glitch/components/copy_paste_text';
 | 
				
			||||||
import { Icon }  from 'flavours/glitch/components/icon';
 | 
					import { Icon }  from 'flavours/glitch/components/icon';
 | 
				
			||||||
import { me, domain } from 'flavours/glitch/initial_state';
 | 
					import { me, domain } from 'flavours/glitch/initial_state';
 | 
				
			||||||
import { useAppSelector } from 'flavours/glitch/store';
 | 
					import { useAppSelector } from 'flavours/glitch/store';
 | 
				
			||||||
| 
						 | 
					@ -20,67 +20,6 @@ const messages = defineMessages({
 | 
				
			||||||
  shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
 | 
					  shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CopyPasteText extends PureComponent {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static propTypes = {
 | 
					 | 
				
			||||||
    value: PropTypes.string,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  state = {
 | 
					 | 
				
			||||||
    copied: false,
 | 
					 | 
				
			||||||
    focused: false,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setRef = c => {
 | 
					 | 
				
			||||||
    this.input = c;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleInputClick = () => {
 | 
					 | 
				
			||||||
    this.setState({ copied: false });
 | 
					 | 
				
			||||||
    this.input.focus();
 | 
					 | 
				
			||||||
    this.input.select();
 | 
					 | 
				
			||||||
    this.input.setSelectionRange(0, this.props.value.length);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleButtonClick = e => {
 | 
					 | 
				
			||||||
    e.stopPropagation();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { value } = this.props;
 | 
					 | 
				
			||||||
    navigator.clipboard.writeText(value);
 | 
					 | 
				
			||||||
    this.input.blur();
 | 
					 | 
				
			||||||
    this.setState({ copied: true });
 | 
					 | 
				
			||||||
    this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleFocus = () => {
 | 
					 | 
				
			||||||
    this.setState({ focused: true });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleBlur = () => {
 | 
					 | 
				
			||||||
    this.setState({ focused: false });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentWillUnmount () {
 | 
					 | 
				
			||||||
    if (this.timeout) clearTimeout(this.timeout);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					 | 
				
			||||||
    const { value } = this.props;
 | 
					 | 
				
			||||||
    const { copied, focused } = this.state;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <div className={classNames('copy-paste-text', { copied, focused })} tabIndex='0' role='button' onClick={this.handleInputClick}>
 | 
					 | 
				
			||||||
        <textarea readOnly value={value} ref={this.setRef} onClick={this.handleInputClick} onFocus={this.handleFocus} onBlur={this.handleBlur} />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <button className='button' onClick={this.handleButtonClick}>
 | 
					 | 
				
			||||||
          <Icon id='copy' icon={ContentCopyIcon} /> {copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : <FormattedMessage id='copypaste.copy_to_clipboard' defaultMessage='Copy to clipboard' />}
 | 
					 | 
				
			||||||
        </button>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class TipCarousel extends PureComponent {
 | 
					class TipCarousel extends PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static propTypes = {
 | 
					  static propTypes = {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,94 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/no-unsafe-return,
 | 
				
			||||||
 | 
					                  @typescript-eslint/no-explicit-any,
 | 
				
			||||||
 | 
					                  @typescript-eslint/no-unsafe-assignment */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useEffect, useCallback } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Provider } from 'react-redux';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  fetchStatus,
 | 
				
			||||||
 | 
					  toggleStatusSpoilers,
 | 
				
			||||||
 | 
					} from 'flavours/glitch/actions/statuses';
 | 
				
			||||||
 | 
					import { hydrateStore } from 'flavours/glitch/actions/store';
 | 
				
			||||||
 | 
					import { Router } from 'flavours/glitch/components/router';
 | 
				
			||||||
 | 
					import { DetailedStatus } from 'flavours/glitch/features/status/components/detailed_status';
 | 
				
			||||||
 | 
					import { useRenderSignal } from 'flavours/glitch/hooks/useRenderSignal';
 | 
				
			||||||
 | 
					import initialState from 'flavours/glitch/initial_state';
 | 
				
			||||||
 | 
					import { IntlProvider } from 'flavours/glitch/locales';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  makeGetStatus,
 | 
				
			||||||
 | 
					  makeGetPictureInPicture,
 | 
				
			||||||
 | 
					} from 'flavours/glitch/selectors';
 | 
				
			||||||
 | 
					import { store, useAppSelector, useAppDispatch } from 'flavours/glitch/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
 | 
				
			||||||
 | 
					const getPictureInPicture = makeGetPictureInPicture() as unknown as (
 | 
				
			||||||
 | 
					  arg0: any,
 | 
				
			||||||
 | 
					  arg1: any,
 | 
				
			||||||
 | 
					) => any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Embed: React.FC<{ id: string }> = ({ id }) => {
 | 
				
			||||||
 | 
					  const status = useAppSelector((state) => getStatus(state, { id }));
 | 
				
			||||||
 | 
					  const pictureInPicture = useAppSelector((state) =>
 | 
				
			||||||
 | 
					    getPictureInPicture(state, { id }),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const domain = useAppSelector((state) => state.meta.get('domain'));
 | 
				
			||||||
 | 
					  const dispatch = useAppDispatch();
 | 
				
			||||||
 | 
					  const dispatchRenderSignal = useRenderSignal();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    dispatch(fetchStatus(id, false, false));
 | 
				
			||||||
 | 
					  }, [dispatch, id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleToggleHidden = useCallback(() => {
 | 
				
			||||||
 | 
					    dispatch(toggleStatusSpoilers(id));
 | 
				
			||||||
 | 
					  }, [dispatch, id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // This allows us to calculate the correct page height for embeds
 | 
				
			||||||
 | 
					  if (status) {
 | 
				
			||||||
 | 
					    dispatchRenderSignal();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
 | 
				
			||||||
 | 
					  const permalink = status?.get('url') as string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className='embed'>
 | 
				
			||||||
 | 
					      <DetailedStatus
 | 
				
			||||||
 | 
					        status={status}
 | 
				
			||||||
 | 
					        domain={domain}
 | 
				
			||||||
 | 
					        pictureInPicture={pictureInPicture}
 | 
				
			||||||
 | 
					        onToggleHidden={handleToggleHidden}
 | 
				
			||||||
 | 
					        expanded={false}
 | 
				
			||||||
 | 
					        withLogo
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <a
 | 
				
			||||||
 | 
					        className='embed__overlay'
 | 
				
			||||||
 | 
					        href={permalink}
 | 
				
			||||||
 | 
					        target='_blank'
 | 
				
			||||||
 | 
					        rel='noreferrer noopener'
 | 
				
			||||||
 | 
					        aria-label=''
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Status: React.FC<{ id: string }> = ({ id }) => {
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (initialState) {
 | 
				
			||||||
 | 
					      store.dispatch(hydrateStore(initialState));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <IntlProvider>
 | 
				
			||||||
 | 
					      <Provider store={store}>
 | 
				
			||||||
 | 
					        <Router>
 | 
				
			||||||
 | 
					          <Embed id={id} />
 | 
				
			||||||
 | 
					        </Router>
 | 
				
			||||||
 | 
					      </Provider>
 | 
				
			||||||
 | 
					    </IntlProvider>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -49,7 +49,7 @@ const messages = defineMessages({
 | 
				
			||||||
  share: { id: 'status.share', defaultMessage: 'Share' },
 | 
					  share: { id: 'status.share', defaultMessage: 'Share' },
 | 
				
			||||||
  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
 | 
					  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
 | 
				
			||||||
  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
 | 
					  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
 | 
				
			||||||
  embed: { id: 'status.embed', defaultMessage: 'Embed' },
 | 
					  embed: { id: 'status.embed', defaultMessage: 'Get embed code' },
 | 
				
			||||||
  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
 | 
					  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
 | 
				
			||||||
  admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
 | 
					  admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
 | 
				
			||||||
  admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
 | 
					  admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,336 +0,0 @@
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { FormattedDate, FormattedMessage } from 'react-intl';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import classNames from 'classnames';
 | 
					 | 
				
			||||||
import { Link, withRouter } from 'react-router-dom';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
 | 
					 | 
				
			||||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
 | 
					 | 
				
			||||||
import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
 | 
					 | 
				
			||||||
import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar';
 | 
					 | 
				
			||||||
import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
 | 
					 | 
				
			||||||
import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
 | 
					 | 
				
			||||||
import PollContainer from 'flavours/glitch/containers/poll_container';
 | 
					 | 
				
			||||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { Avatar } from '../../../components/avatar';
 | 
					 | 
				
			||||||
import { DisplayName } from '../../../components/display_name';
 | 
					 | 
				
			||||||
import MediaGallery from '../../../components/media_gallery';
 | 
					 | 
				
			||||||
import StatusContent from '../../../components/status_content';
 | 
					 | 
				
			||||||
import Audio from '../../audio';
 | 
					 | 
				
			||||||
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
 | 
					 | 
				
			||||||
import Video from '../../video';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import Card from './card';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class DetailedStatus extends ImmutablePureComponent {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static propTypes = {
 | 
					 | 
				
			||||||
    status: ImmutablePropTypes.map,
 | 
					 | 
				
			||||||
    settings: ImmutablePropTypes.map.isRequired,
 | 
					 | 
				
			||||||
    onOpenMedia: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    onOpenVideo: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    onToggleHidden: PropTypes.func,
 | 
					 | 
				
			||||||
    onTranslate: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    expanded: PropTypes.bool,
 | 
					 | 
				
			||||||
    measureHeight: PropTypes.bool,
 | 
					 | 
				
			||||||
    onHeightChange: PropTypes.func,
 | 
					 | 
				
			||||||
    domain: PropTypes.string.isRequired,
 | 
					 | 
				
			||||||
    compact: PropTypes.bool,
 | 
					 | 
				
			||||||
    showMedia: PropTypes.bool,
 | 
					 | 
				
			||||||
    pictureInPicture: ImmutablePropTypes.contains({
 | 
					 | 
				
			||||||
      inUse: PropTypes.bool,
 | 
					 | 
				
			||||||
      available: PropTypes.bool,
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
    onToggleMediaVisibility: PropTypes.func,
 | 
					 | 
				
			||||||
    ...WithRouterPropTypes,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  state = {
 | 
					 | 
				
			||||||
    height: null,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleAccountClick = (e) => {
 | 
					 | 
				
			||||||
    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.props.history) {
 | 
					 | 
				
			||||||
      e.preventDefault();
 | 
					 | 
				
			||||||
      this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    e.stopPropagation();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  parseClick = (e, destination) => {
 | 
					 | 
				
			||||||
    if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.props.history) {
 | 
					 | 
				
			||||||
      e.preventDefault();
 | 
					 | 
				
			||||||
      this.props.history.push(destination);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    e.stopPropagation();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleOpenVideo = (options) => {
 | 
					 | 
				
			||||||
    this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  _measureHeight (heightJustChanged) {
 | 
					 | 
				
			||||||
    if (this.props.measureHeight && this.node) {
 | 
					 | 
				
			||||||
      scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (this.props.onHeightChange && heightJustChanged) {
 | 
					 | 
				
			||||||
        this.props.onHeightChange();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setRef = c => {
 | 
					 | 
				
			||||||
    this.node = c;
 | 
					 | 
				
			||||||
    this._measureHeight();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentDidUpdate (prevProps, prevState) {
 | 
					 | 
				
			||||||
    this._measureHeight(prevState.height !== this.state.height);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleChildUpdate = () => {
 | 
					 | 
				
			||||||
    this._measureHeight();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleModalLink = 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');
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleTranslate = () => {
 | 
					 | 
				
			||||||
    const { onTranslate, status } = this.props;
 | 
					 | 
				
			||||||
    onTranslate(status);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					 | 
				
			||||||
    const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
 | 
					 | 
				
			||||||
    const outerStyle = { boxSizing: 'border-box' };
 | 
					 | 
				
			||||||
    const { compact, pictureInPicture, expanded, onToggleHidden, settings } = this.props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!status) {
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let applicationLink = '';
 | 
					 | 
				
			||||||
    let reblogLink = '';
 | 
					 | 
				
			||||||
    let favouriteLink = '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    //  Depending on user settings, some media are considered as parts of the
 | 
					 | 
				
			||||||
    //  contents (affected by CW) while other will be displayed outside of the
 | 
					 | 
				
			||||||
    //  CW.
 | 
					 | 
				
			||||||
    let contentMedia = [];
 | 
					 | 
				
			||||||
    let contentMediaIcons = [];
 | 
					 | 
				
			||||||
    let extraMedia = [];
 | 
					 | 
				
			||||||
    let extraMediaIcons = [];
 | 
					 | 
				
			||||||
    let media = contentMedia;
 | 
					 | 
				
			||||||
    let mediaIcons = contentMediaIcons;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (settings.getIn(['content_warnings', 'media_outside'])) {
 | 
					 | 
				
			||||||
      media = extraMedia;
 | 
					 | 
				
			||||||
      mediaIcons = extraMediaIcons;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.props.measureHeight) {
 | 
					 | 
				
			||||||
      outerStyle.height = `${this.state.height}px`;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const language = status.getIn(['translation', 'language']) || status.get('language');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (pictureInPicture.get('inUse')) {
 | 
					 | 
				
			||||||
      media.push(<PictureInPicturePlaceholder />);
 | 
					 | 
				
			||||||
      mediaIcons.push('video-camera');
 | 
					 | 
				
			||||||
    } else if (status.get('media_attachments').size > 0) {
 | 
					 | 
				
			||||||
      if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) {
 | 
					 | 
				
			||||||
        media.push(<AttachmentList media={status.get('media_attachments')} />);
 | 
					 | 
				
			||||||
      } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
 | 
					 | 
				
			||||||
        const attachment = status.getIn(['media_attachments', 0]);
 | 
					 | 
				
			||||||
        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        media.push(
 | 
					 | 
				
			||||||
          <Audio
 | 
					 | 
				
			||||||
            src={attachment.get('url')}
 | 
					 | 
				
			||||||
            alt={description}
 | 
					 | 
				
			||||||
            lang={language}
 | 
					 | 
				
			||||||
            duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
 | 
					 | 
				
			||||||
            poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
 | 
					 | 
				
			||||||
            backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
 | 
					 | 
				
			||||||
            foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
 | 
					 | 
				
			||||||
            accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
 | 
					 | 
				
			||||||
            sensitive={status.get('sensitive')}
 | 
					 | 
				
			||||||
            visible={this.props.showMedia}
 | 
					 | 
				
			||||||
            blurhash={attachment.get('blurhash')}
 | 
					 | 
				
			||||||
            height={150}
 | 
					 | 
				
			||||||
            onToggleVisibility={this.props.onToggleMediaVisibility}
 | 
					 | 
				
			||||||
          />,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        mediaIcons.push('music');
 | 
					 | 
				
			||||||
      } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
 | 
					 | 
				
			||||||
        const attachment = status.getIn(['media_attachments', 0]);
 | 
					 | 
				
			||||||
        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        media.push(
 | 
					 | 
				
			||||||
          <Video
 | 
					 | 
				
			||||||
            preview={attachment.get('preview_url')}
 | 
					 | 
				
			||||||
            frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
 | 
					 | 
				
			||||||
            blurhash={attachment.get('blurhash')}
 | 
					 | 
				
			||||||
            src={attachment.get('url')}
 | 
					 | 
				
			||||||
            alt={description}
 | 
					 | 
				
			||||||
            lang={language}
 | 
					 | 
				
			||||||
            inline
 | 
					 | 
				
			||||||
            sensitive={status.get('sensitive')}
 | 
					 | 
				
			||||||
            letterbox={settings.getIn(['media', 'letterbox'])}
 | 
					 | 
				
			||||||
            fullwidth={settings.getIn(['media', 'fullwidth'])}
 | 
					 | 
				
			||||||
            preventPlayback={!expanded}
 | 
					 | 
				
			||||||
            onOpenVideo={this.handleOpenVideo}
 | 
					 | 
				
			||||||
            autoplay
 | 
					 | 
				
			||||||
            visible={this.props.showMedia}
 | 
					 | 
				
			||||||
            onToggleVisibility={this.props.onToggleMediaVisibility}
 | 
					 | 
				
			||||||
          />,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        mediaIcons.push('video-camera');
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        media.push(
 | 
					 | 
				
			||||||
          <MediaGallery
 | 
					 | 
				
			||||||
            standalone
 | 
					 | 
				
			||||||
            sensitive={status.get('sensitive')}
 | 
					 | 
				
			||||||
            media={status.get('media_attachments')}
 | 
					 | 
				
			||||||
            lang={language}
 | 
					 | 
				
			||||||
            letterbox={settings.getIn(['media', 'letterbox'])}
 | 
					 | 
				
			||||||
            fullwidth={settings.getIn(['media', 'fullwidth'])}
 | 
					 | 
				
			||||||
            hidden={!expanded}
 | 
					 | 
				
			||||||
            onOpenMedia={this.props.onOpenMedia}
 | 
					 | 
				
			||||||
            visible={this.props.showMedia}
 | 
					 | 
				
			||||||
            onToggleVisibility={this.props.onToggleMediaVisibility}
 | 
					 | 
				
			||||||
          />,
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        mediaIcons.push('picture-o');
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else if (status.get('card')) {
 | 
					 | 
				
			||||||
      media.push(<Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card')} />);
 | 
					 | 
				
			||||||
      mediaIcons.push('link');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (status.get('poll')) {
 | 
					 | 
				
			||||||
      contentMedia.push(<PollContainer pollId={status.get('poll')} lang={status.get('language')} />);
 | 
					 | 
				
			||||||
      contentMediaIcons.push('tasks');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (status.get('application')) {
 | 
					 | 
				
			||||||
      applicationLink = <>·<a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></>;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const visibilityLink = <>·<VisibilityIcon visibility={status.get('visibility')} /></>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!['unlisted', 'public'].includes(status.get('visibility'))) {
 | 
					 | 
				
			||||||
      reblogLink = null;
 | 
					 | 
				
			||||||
    } else if (this.props.history) {
 | 
					 | 
				
			||||||
      reblogLink = (
 | 
					 | 
				
			||||||
        <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
 | 
					 | 
				
			||||||
          <span className='detailed-status__reblogs'>
 | 
					 | 
				
			||||||
            <AnimatedNumber value={status.get('reblogs_count')} />
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
          <FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
 | 
					 | 
				
			||||||
        </Link>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      reblogLink = (
 | 
					 | 
				
			||||||
        <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
 | 
					 | 
				
			||||||
          <span className='detailed-status__reblogs'>
 | 
					 | 
				
			||||||
            <AnimatedNumber value={status.get('reblogs_count')} />
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
          <FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
 | 
					 | 
				
			||||||
        </a>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.props.history) {
 | 
					 | 
				
			||||||
      favouriteLink = (
 | 
					 | 
				
			||||||
        <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
 | 
					 | 
				
			||||||
          <span className='detailed-status__favorites'>
 | 
					 | 
				
			||||||
            <AnimatedNumber value={status.get('favourites_count')} />
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
          <FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
 | 
					 | 
				
			||||||
        </Link>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      favouriteLink = (
 | 
					 | 
				
			||||||
        <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
 | 
					 | 
				
			||||||
          <span className='detailed-status__favorites'>
 | 
					 | 
				
			||||||
            <AnimatedNumber value={status.get('favourites_count')} />
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
          <FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
 | 
					 | 
				
			||||||
        </a>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
 | 
					 | 
				
			||||||
    contentMedia.push(hashtagBar);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <div style={outerStyle}>
 | 
					 | 
				
			||||||
        <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })} data-status-by={status.getIn(['account', 'acct'])}>
 | 
					 | 
				
			||||||
          <a href={status.getIn(['account', 'url'])} data-hover-card-account={status.getIn(['account', 'id'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
 | 
					 | 
				
			||||||
            <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={48} /></div>
 | 
					 | 
				
			||||||
            <DisplayName account={status.get('account')} localDomain={this.props.domain} />
 | 
					 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <StatusContent
 | 
					 | 
				
			||||||
            status={status}
 | 
					 | 
				
			||||||
            media={contentMedia}
 | 
					 | 
				
			||||||
            extraMedia={extraMedia}
 | 
					 | 
				
			||||||
            mediaIcons={contentMediaIcons}
 | 
					 | 
				
			||||||
            expanded={expanded}
 | 
					 | 
				
			||||||
            collapsed={false}
 | 
					 | 
				
			||||||
            onExpandedToggle={onToggleHidden}
 | 
					 | 
				
			||||||
            onTranslate={this.handleTranslate}
 | 
					 | 
				
			||||||
            parseClick={this.parseClick}
 | 
					 | 
				
			||||||
            onUpdate={this.handleChildUpdate}
 | 
					 | 
				
			||||||
            tagLinks={settings.get('tag_misleading_links')}
 | 
					 | 
				
			||||||
            rewriteMentions={settings.get('rewrite_mentions')}
 | 
					 | 
				
			||||||
            disabled
 | 
					 | 
				
			||||||
            {...statusContentProps}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div className='detailed-status__meta'>
 | 
					 | 
				
			||||||
            <div className='detailed-status__meta__line'>
 | 
					 | 
				
			||||||
              <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
 | 
					 | 
				
			||||||
                <FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
 | 
					 | 
				
			||||||
              </a>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              {visibilityLink}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              {applicationLink}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            {status.get('edited_at') && <div className='detailed-status__meta__line'><EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} /></div>}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div className='detailed-status__meta__line'>
 | 
					 | 
				
			||||||
              {reblogLink}
 | 
					 | 
				
			||||||
              {reblogLink && <>·</>}
 | 
					 | 
				
			||||||
              {favouriteLink}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default withRouter(DetailedStatus);
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,427 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/no-unsafe-member-access,
 | 
				
			||||||
 | 
					                  @typescript-eslint/no-unsafe-call,
 | 
				
			||||||
 | 
					                  @typescript-eslint/no-explicit-any,
 | 
				
			||||||
 | 
					                  @typescript-eslint/no-unsafe-assignment */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { CSSProperties } from 'react';
 | 
				
			||||||
 | 
					import { useState, useRef, useCallback } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { FormattedDate, FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					import { Link } from 'react-router-dom';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { AnimatedNumber } from 'flavours/glitch/components/animated_number';
 | 
				
			||||||
 | 
					import AttachmentList from 'flavours/glitch/components/attachment_list';
 | 
				
			||||||
 | 
					import EditedTimestamp from 'flavours/glitch/components/edited_timestamp';
 | 
				
			||||||
 | 
					import type { StatusLike } from 'flavours/glitch/components/hashtag_bar';
 | 
				
			||||||
 | 
					import { getHashtagBarForStatus } from 'flavours/glitch/components/hashtag_bar';
 | 
				
			||||||
 | 
					import { IconLogo } from 'flavours/glitch/components/logo';
 | 
				
			||||||
 | 
					import { Permalink } from 'flavours/glitch/components/permalink';
 | 
				
			||||||
 | 
					import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder';
 | 
				
			||||||
 | 
					import { useAppHistory } from 'flavours/glitch/components/router';
 | 
				
			||||||
 | 
					import { VisibilityIcon } from 'flavours/glitch/components/visibility_icon';
 | 
				
			||||||
 | 
					import { useAppSelector } from 'flavours/glitch/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Avatar } from '../../../components/avatar';
 | 
				
			||||||
 | 
					import { DisplayName } from '../../../components/display_name';
 | 
				
			||||||
 | 
					import MediaGallery from '../../../components/media_gallery';
 | 
				
			||||||
 | 
					import StatusContent from '../../../components/status_content';
 | 
				
			||||||
 | 
					import Audio from '../../audio';
 | 
				
			||||||
 | 
					import scheduleIdleTask from '../../ui/util/schedule_idle_task';
 | 
				
			||||||
 | 
					import Video from '../../video';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Card from './card';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface VideoModalOptions {
 | 
				
			||||||
 | 
					  startTime: number;
 | 
				
			||||||
 | 
					  autoPlay?: boolean;
 | 
				
			||||||
 | 
					  defaultVolume: number;
 | 
				
			||||||
 | 
					  componentIndex: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DetailedStatus: React.FC<{
 | 
				
			||||||
 | 
					  status: any;
 | 
				
			||||||
 | 
					  onOpenMedia?: (status: any, index: number, lang: string) => void;
 | 
				
			||||||
 | 
					  onOpenVideo?: (status: any, lang: string, options: VideoModalOptions) => void;
 | 
				
			||||||
 | 
					  onTranslate?: (status: any) => void;
 | 
				
			||||||
 | 
					  measureHeight?: boolean;
 | 
				
			||||||
 | 
					  onHeightChange?: () => void;
 | 
				
			||||||
 | 
					  domain: string;
 | 
				
			||||||
 | 
					  showMedia?: boolean;
 | 
				
			||||||
 | 
					  withLogo?: boolean;
 | 
				
			||||||
 | 
					  pictureInPicture: any;
 | 
				
			||||||
 | 
					  onToggleHidden?: (status: any) => void;
 | 
				
			||||||
 | 
					  onToggleMediaVisibility?: () => void;
 | 
				
			||||||
 | 
					  expanded: boolean;
 | 
				
			||||||
 | 
					}> = ({
 | 
				
			||||||
 | 
					  status,
 | 
				
			||||||
 | 
					  onOpenMedia,
 | 
				
			||||||
 | 
					  onOpenVideo,
 | 
				
			||||||
 | 
					  onTranslate,
 | 
				
			||||||
 | 
					  measureHeight,
 | 
				
			||||||
 | 
					  onHeightChange,
 | 
				
			||||||
 | 
					  domain,
 | 
				
			||||||
 | 
					  showMedia,
 | 
				
			||||||
 | 
					  withLogo,
 | 
				
			||||||
 | 
					  pictureInPicture,
 | 
				
			||||||
 | 
					  onToggleMediaVisibility,
 | 
				
			||||||
 | 
					  onToggleHidden,
 | 
				
			||||||
 | 
					  expanded,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					  const properStatus = status?.get('reblog') ?? status;
 | 
				
			||||||
 | 
					  const [height, setHeight] = useState(0);
 | 
				
			||||||
 | 
					  const nodeRef = useRef<HTMLDivElement>();
 | 
				
			||||||
 | 
					  const history = useAppHistory();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const rewriteMentions = useAppSelector(
 | 
				
			||||||
 | 
					    (state) => state.local_settings.get('rewrite_mentions', false) as boolean,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const tagMisleadingLinks = useAppSelector(
 | 
				
			||||||
 | 
					    (state) =>
 | 
				
			||||||
 | 
					      state.local_settings.get('tag_misleading_links', false) as boolean,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const mediaOutsideCW = useAppSelector(
 | 
				
			||||||
 | 
					    (state) =>
 | 
				
			||||||
 | 
					      state.local_settings.getIn(
 | 
				
			||||||
 | 
					        ['content_warnings', 'media_outside'],
 | 
				
			||||||
 | 
					        false,
 | 
				
			||||||
 | 
					      ) as boolean,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const letterboxMedia = useAppSelector(
 | 
				
			||||||
 | 
					    (state) =>
 | 
				
			||||||
 | 
					      state.local_settings.getIn(['media', 'letterbox'], false) as boolean,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const fullwidthMedia = useAppSelector(
 | 
				
			||||||
 | 
					    (state) =>
 | 
				
			||||||
 | 
					      state.local_settings.getIn(['media', 'fullwidth'], false) as boolean,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleOpenVideo = useCallback(
 | 
				
			||||||
 | 
					    (options: VideoModalOptions) => {
 | 
				
			||||||
 | 
					      const lang = (status.getIn(['translation', 'language']) ||
 | 
				
			||||||
 | 
					        status.get('language')) as string;
 | 
				
			||||||
 | 
					      if (onOpenVideo)
 | 
				
			||||||
 | 
					        onOpenVideo(status.getIn(['media_attachments', 0]), lang, options);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [onOpenVideo, status],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _measureHeight = useCallback(
 | 
				
			||||||
 | 
					    (heightJustChanged?: boolean) => {
 | 
				
			||||||
 | 
					      if (measureHeight && nodeRef.current) {
 | 
				
			||||||
 | 
					        scheduleIdleTask(() => {
 | 
				
			||||||
 | 
					          if (nodeRef.current)
 | 
				
			||||||
 | 
					            setHeight(Math.ceil(nodeRef.current.scrollHeight) + 1);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (onHeightChange && heightJustChanged) {
 | 
				
			||||||
 | 
					          onHeightChange();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [onHeightChange, measureHeight, setHeight],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleRef = useCallback(
 | 
				
			||||||
 | 
					    (c: HTMLDivElement) => {
 | 
				
			||||||
 | 
					      nodeRef.current = c;
 | 
				
			||||||
 | 
					      _measureHeight();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [_measureHeight],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleChildUpdate = useCallback(() => {
 | 
				
			||||||
 | 
					    _measureHeight();
 | 
				
			||||||
 | 
					  }, [_measureHeight]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleTranslate = useCallback(() => {
 | 
				
			||||||
 | 
					    if (onTranslate) onTranslate(status);
 | 
				
			||||||
 | 
					  }, [onTranslate, status]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const parseClick = useCallback(
 | 
				
			||||||
 | 
					    (e: React.MouseEvent, destination: string) => {
 | 
				
			||||||
 | 
					      if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        history.push(destination);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      e.stopPropagation();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [history],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!properStatus) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let applicationLink;
 | 
				
			||||||
 | 
					  let reblogLink;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //  Depending on user settings, some media are considered as parts of the
 | 
				
			||||||
 | 
					  //  contents (affected by CW) while other will be displayed outside of the
 | 
				
			||||||
 | 
					  //  CW.
 | 
				
			||||||
 | 
					  const contentMedia: React.ReactNode[] = [];
 | 
				
			||||||
 | 
					  const contentMediaIcons: string[] = [];
 | 
				
			||||||
 | 
					  const extraMedia: React.ReactNode[] = [];
 | 
				
			||||||
 | 
					  const extraMediaIcons: string[] = [];
 | 
				
			||||||
 | 
					  let media = contentMedia;
 | 
				
			||||||
 | 
					  let mediaIcons: string[] = contentMediaIcons;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (mediaOutsideCW) {
 | 
				
			||||||
 | 
					    media = extraMedia;
 | 
				
			||||||
 | 
					    mediaIcons = extraMediaIcons;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const outerStyle = { boxSizing: 'border-box' } as CSSProperties;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (measureHeight) {
 | 
				
			||||||
 | 
					    outerStyle.height = height;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const language =
 | 
				
			||||||
 | 
					    status.getIn(['translation', 'language']) || status.get('language');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (pictureInPicture.get('inUse')) {
 | 
				
			||||||
 | 
					    media.push(<PictureInPicturePlaceholder />);
 | 
				
			||||||
 | 
					    mediaIcons.push('video-camera');
 | 
				
			||||||
 | 
					  } else if (status.get('media_attachments').size > 0) {
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      status
 | 
				
			||||||
 | 
					        .get('media_attachments')
 | 
				
			||||||
 | 
					        .some(
 | 
				
			||||||
 | 
					          (item: Immutable.Map<string, any>) => item.get('type') === 'unknown',
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      media.push(<AttachmentList media={status.get('media_attachments')} />);
 | 
				
			||||||
 | 
					    } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
 | 
				
			||||||
 | 
					      const attachment = status.getIn(['media_attachments', 0]);
 | 
				
			||||||
 | 
					      const description =
 | 
				
			||||||
 | 
					        attachment.getIn(['translation', 'description']) ||
 | 
				
			||||||
 | 
					        attachment.get('description');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      media.push(
 | 
				
			||||||
 | 
					        <Audio
 | 
				
			||||||
 | 
					          src={attachment.get('url')}
 | 
				
			||||||
 | 
					          alt={description}
 | 
				
			||||||
 | 
					          lang={language}
 | 
				
			||||||
 | 
					          duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
 | 
				
			||||||
 | 
					          poster={
 | 
				
			||||||
 | 
					            attachment.get('preview_url') ||
 | 
				
			||||||
 | 
					            status.getIn(['account', 'avatar_static'])
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
 | 
				
			||||||
 | 
					          foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
 | 
				
			||||||
 | 
					          accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
 | 
				
			||||||
 | 
					          sensitive={status.get('sensitive')}
 | 
				
			||||||
 | 
					          visible={showMedia}
 | 
				
			||||||
 | 
					          blurhash={attachment.get('blurhash')}
 | 
				
			||||||
 | 
					          height={150}
 | 
				
			||||||
 | 
					          onToggleVisibility={onToggleMediaVisibility}
 | 
				
			||||||
 | 
					        />,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      mediaIcons.push('music');
 | 
				
			||||||
 | 
					    } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
 | 
				
			||||||
 | 
					      const attachment = status.getIn(['media_attachments', 0]);
 | 
				
			||||||
 | 
					      const description =
 | 
				
			||||||
 | 
					        attachment.getIn(['translation', 'description']) ||
 | 
				
			||||||
 | 
					        attachment.get('description');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      media.push(
 | 
				
			||||||
 | 
					        <Video
 | 
				
			||||||
 | 
					          preview={attachment.get('preview_url')}
 | 
				
			||||||
 | 
					          frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
 | 
				
			||||||
 | 
					          aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`}
 | 
				
			||||||
 | 
					          blurhash={attachment.get('blurhash')}
 | 
				
			||||||
 | 
					          src={attachment.get('url')}
 | 
				
			||||||
 | 
					          alt={description}
 | 
				
			||||||
 | 
					          lang={language}
 | 
				
			||||||
 | 
					          width={300}
 | 
				
			||||||
 | 
					          height={150}
 | 
				
			||||||
 | 
					          onOpenVideo={handleOpenVideo}
 | 
				
			||||||
 | 
					          sensitive={status.get('sensitive')}
 | 
				
			||||||
 | 
					          visible={showMedia}
 | 
				
			||||||
 | 
					          onToggleVisibility={onToggleMediaVisibility}
 | 
				
			||||||
 | 
					          letterbox={letterboxMedia}
 | 
				
			||||||
 | 
					          fullwidth={fullwidthMedia}
 | 
				
			||||||
 | 
					          preventPlayback={!expanded}
 | 
				
			||||||
 | 
					        />,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      mediaIcons.push('video-camera');
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      media.push(
 | 
				
			||||||
 | 
					        <MediaGallery
 | 
				
			||||||
 | 
					          standalone
 | 
				
			||||||
 | 
					          sensitive={status.get('sensitive')}
 | 
				
			||||||
 | 
					          media={status.get('media_attachments')}
 | 
				
			||||||
 | 
					          lang={language}
 | 
				
			||||||
 | 
					          height={300}
 | 
				
			||||||
 | 
					          letterbox={letterboxMedia}
 | 
				
			||||||
 | 
					          fullwidth={fullwidthMedia}
 | 
				
			||||||
 | 
					          hidden={!expanded}
 | 
				
			||||||
 | 
					          onOpenMedia={onOpenMedia}
 | 
				
			||||||
 | 
					          visible={showMedia}
 | 
				
			||||||
 | 
					          onToggleVisibility={onToggleMediaVisibility}
 | 
				
			||||||
 | 
					        />,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      mediaIcons.push('picture-o');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else if (status.get('spoiler_text').length === 0) {
 | 
				
			||||||
 | 
					    media.push(
 | 
				
			||||||
 | 
					      <Card
 | 
				
			||||||
 | 
					        sensitive={status.get('sensitive')}
 | 
				
			||||||
 | 
					        onOpenMedia={onOpenMedia}
 | 
				
			||||||
 | 
					        card={status.get('card', null)}
 | 
				
			||||||
 | 
					      />,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    mediaIcons.push('link');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (status.get('application')) {
 | 
				
			||||||
 | 
					    applicationLink = (
 | 
				
			||||||
 | 
					      <>
 | 
				
			||||||
 | 
					        ·
 | 
				
			||||||
 | 
					        <a
 | 
				
			||||||
 | 
					          className='detailed-status__application'
 | 
				
			||||||
 | 
					          href={status.getIn(['application', 'website'])}
 | 
				
			||||||
 | 
					          target='_blank'
 | 
				
			||||||
 | 
					          rel='noopener noreferrer'
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {status.getIn(['application', 'name'])}
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					      </>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const visibilityLink = (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      ·<VisibilityIcon visibility={status.get('visibility')} />
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (['private', 'direct'].includes(status.get('visibility') as string)) {
 | 
				
			||||||
 | 
					    reblogLink = '';
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    reblogLink = (
 | 
				
			||||||
 | 
					      <Link
 | 
				
			||||||
 | 
					        to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`}
 | 
				
			||||||
 | 
					        className='detailed-status__link'
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <span className='detailed-status__reblogs'>
 | 
				
			||||||
 | 
					          <AnimatedNumber value={status.get('reblogs_count')} />
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					        <FormattedMessage
 | 
				
			||||||
 | 
					          id='status.reblogs'
 | 
				
			||||||
 | 
					          defaultMessage='{count, plural, one {boost} other {boosts}}'
 | 
				
			||||||
 | 
					          values={{ count: status.get('reblogs_count') }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </Link>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const favouriteLink = (
 | 
				
			||||||
 | 
					    <Link
 | 
				
			||||||
 | 
					      to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`}
 | 
				
			||||||
 | 
					      className='detailed-status__link'
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <span className='detailed-status__favorites'>
 | 
				
			||||||
 | 
					        <AnimatedNumber value={status.get('favourites_count')} />
 | 
				
			||||||
 | 
					      </span>
 | 
				
			||||||
 | 
					      <FormattedMessage
 | 
				
			||||||
 | 
					        id='status.favourites'
 | 
				
			||||||
 | 
					        defaultMessage='{count, plural, one {favorite} other {favorites}}'
 | 
				
			||||||
 | 
					        values={{ count: status.get('favourites_count') }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </Link>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { statusContentProps, hashtagBar } = getHashtagBarForStatus(
 | 
				
			||||||
 | 
					    status as StatusLike,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  contentMedia.push(hashtagBar);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div style={outerStyle}>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        ref={handleRef}
 | 
				
			||||||
 | 
					        className={classNames(
 | 
				
			||||||
 | 
					          'detailed-status',
 | 
				
			||||||
 | 
					          `detailed-status-${status.get('visibility')}`,
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        data-status-by={status.getIn(['account', 'acct'])}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Permalink
 | 
				
			||||||
 | 
					          to={`/@${status.getIn(['account', 'acct'])}`}
 | 
				
			||||||
 | 
					          href={status.getIn(['account', 'url'])}
 | 
				
			||||||
 | 
					          data-hover-card-account={status.getIn(['account', 'id'])}
 | 
				
			||||||
 | 
					          className='detailed-status__display-name'
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className='detailed-status__display-avatar'>
 | 
				
			||||||
 | 
					            <Avatar account={status.get('account')} size={46} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <DisplayName account={status.get('account')} localDomain={domain} />
 | 
				
			||||||
 | 
					          {withLogo && (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					              <div className='spacer' />
 | 
				
			||||||
 | 
					              <IconLogo />
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </Permalink>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <StatusContent
 | 
				
			||||||
 | 
					          status={status}
 | 
				
			||||||
 | 
					          media={contentMedia}
 | 
				
			||||||
 | 
					          extraMedia={extraMedia}
 | 
				
			||||||
 | 
					          mediaIcons={contentMediaIcons}
 | 
				
			||||||
 | 
					          expanded={expanded}
 | 
				
			||||||
 | 
					          collapsed={false}
 | 
				
			||||||
 | 
					          onExpandedToggle={onToggleHidden}
 | 
				
			||||||
 | 
					          onTranslate={handleTranslate}
 | 
				
			||||||
 | 
					          onUpdate={handleChildUpdate}
 | 
				
			||||||
 | 
					          tagLinks={tagMisleadingLinks}
 | 
				
			||||||
 | 
					          rewriteMentions={rewriteMentions}
 | 
				
			||||||
 | 
					          parseClick={parseClick}
 | 
				
			||||||
 | 
					          {...(statusContentProps as any)}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className='detailed-status__meta'>
 | 
				
			||||||
 | 
					          <div className='detailed-status__meta__line'>
 | 
				
			||||||
 | 
					            <a
 | 
				
			||||||
 | 
					              className='detailed-status__datetime'
 | 
				
			||||||
 | 
					              href={status.get('url')}
 | 
				
			||||||
 | 
					              target='_blank'
 | 
				
			||||||
 | 
					              rel='noopener noreferrer'
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <FormattedDate
 | 
				
			||||||
 | 
					                value={new Date(status.get('created_at') as string)}
 | 
				
			||||||
 | 
					                year='numeric'
 | 
				
			||||||
 | 
					                month='short'
 | 
				
			||||||
 | 
					                day='2-digit'
 | 
				
			||||||
 | 
					                hour='2-digit'
 | 
				
			||||||
 | 
					                minute='2-digit'
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {visibilityLink}
 | 
				
			||||||
 | 
					            {applicationLink}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {status.get('edited_at') && (
 | 
				
			||||||
 | 
					            <div className='detailed-status__meta__line'>
 | 
				
			||||||
 | 
					              <EditedTimestamp
 | 
				
			||||||
 | 
					                statusId={status.get('id')}
 | 
				
			||||||
 | 
					                timestamp={status.get('edited_at')}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className='detailed-status__meta__line'>
 | 
				
			||||||
 | 
					            {reblogLink}
 | 
				
			||||||
 | 
					            {reblogLink && <>·</>}
 | 
				
			||||||
 | 
					            {favouriteLink}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -1,134 +0,0 @@
 | 
				
			||||||
import { injectIntl } from 'react-intl';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { showAlertForError } from '../../../actions/alerts';
 | 
					 | 
				
			||||||
import { initBlockModal } from '../../../actions/blocks';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  replyCompose,
 | 
					 | 
				
			||||||
  mentionCompose,
 | 
					 | 
				
			||||||
  directCompose,
 | 
					 | 
				
			||||||
} from '../../../actions/compose';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  toggleReblog,
 | 
					 | 
				
			||||||
  toggleFavourite,
 | 
					 | 
				
			||||||
  pin,
 | 
					 | 
				
			||||||
  unpin,
 | 
					 | 
				
			||||||
} from '../../../actions/interactions';
 | 
					 | 
				
			||||||
import { openModal } from '../../../actions/modal';
 | 
					 | 
				
			||||||
import { initMuteModal } from '../../../actions/mutes';
 | 
					 | 
				
			||||||
import { initReport } from '../../../actions/reports';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  muteStatus,
 | 
					 | 
				
			||||||
  unmuteStatus,
 | 
					 | 
				
			||||||
  deleteStatus,
 | 
					 | 
				
			||||||
} from '../../../actions/statuses';
 | 
					 | 
				
			||||||
import { deleteModal } from '../../../initial_state';
 | 
					 | 
				
			||||||
import { makeGetStatus } from '../../../selectors';
 | 
					 | 
				
			||||||
import DetailedStatus from '../components/detailed_status';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const makeMapStateToProps = () => {
 | 
					 | 
				
			||||||
  const getStatus = makeGetStatus();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const mapStateToProps = (state, props) => ({
 | 
					 | 
				
			||||||
    status: getStatus(state, props),
 | 
					 | 
				
			||||||
    domain: state.getIn(['meta', 'domain']),
 | 
					 | 
				
			||||||
    settings: state.get('local_settings'),
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return mapStateToProps;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const mapDispatchToProps = (dispatch) => ({
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onReply (status) {
 | 
					 | 
				
			||||||
    dispatch((_, getState) => {
 | 
					 | 
				
			||||||
      let state = getState();
 | 
					 | 
				
			||||||
      if (state.getIn(['compose', 'text']).trim().length !== 0) {
 | 
					 | 
				
			||||||
        dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        dispatch(replyCompose(status));
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onReblog (status, e) {
 | 
					 | 
				
			||||||
    dispatch(toggleReblog(status.get('id'), e.shiftKey));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onFavourite (status, e) {
 | 
					 | 
				
			||||||
    dispatch(toggleFavourite(status.get('id'), e.shiftKey));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onPin (status) {
 | 
					 | 
				
			||||||
    if (status.get('pinned')) {
 | 
					 | 
				
			||||||
      dispatch(unpin(status));
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      dispatch(pin(status));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onEmbed (status) {
 | 
					 | 
				
			||||||
    dispatch(openModal({
 | 
					 | 
				
			||||||
      modalType: 'EMBED',
 | 
					 | 
				
			||||||
      modalProps: {
 | 
					 | 
				
			||||||
        id: status.get('id'),
 | 
					 | 
				
			||||||
        onError: error => dispatch(showAlertForError(error)),
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onDelete (status, withRedraft = false) {
 | 
					 | 
				
			||||||
    if (!deleteModal) {
 | 
					 | 
				
			||||||
      dispatch(deleteStatus(status.get('id'), withRedraft));
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onDirect (account) {
 | 
					 | 
				
			||||||
    dispatch(directCompose(account));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onMention (account) {
 | 
					 | 
				
			||||||
    dispatch(mentionCompose(account));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onOpenMedia (media, index, lang) {
 | 
					 | 
				
			||||||
    dispatch(openModal({
 | 
					 | 
				
			||||||
      modalType: 'MEDIA',
 | 
					 | 
				
			||||||
      modalProps: { media, index, lang },
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onOpenVideo (media, lang, options) {
 | 
					 | 
				
			||||||
    dispatch(openModal({
 | 
					 | 
				
			||||||
      modalType: 'VIDEO',
 | 
					 | 
				
			||||||
      modalProps: { media, lang, options },
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onBlock (status) {
 | 
					 | 
				
			||||||
    const account = status.get('account');
 | 
					 | 
				
			||||||
    dispatch(initBlockModal(account));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onReport (status) {
 | 
					 | 
				
			||||||
    dispatch(initReport(status.get('account'), status));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onMute (account) {
 | 
					 | 
				
			||||||
    dispatch(initMuteModal(account));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onMuteConversation (status) {
 | 
					 | 
				
			||||||
    if (status.get('muted')) {
 | 
					 | 
				
			||||||
      dispatch(unmuteStatus(status.get('id')));
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      dispatch(muteStatus(status.get('id')));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus));
 | 
					 | 
				
			||||||
| 
						 | 
					@ -63,7 +63,7 @@ import Column from '../ui/components/column';
 | 
				
			||||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
 | 
					import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ActionBar from './components/action_bar';
 | 
					import ActionBar from './components/action_bar';
 | 
				
			||||||
import DetailedStatus from './components/detailed_status';
 | 
					import { DetailedStatus } from './components/detailed_status';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -99,7 +99,7 @@ export const BlockModal = ({ accountId, acct }) => {
 | 
				
			||||||
            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
 | 
					            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Button onClick={handleClick}>
 | 
					          <Button onClick={handleClick} autoFocus>
 | 
				
			||||||
            <FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
 | 
					            <FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -79,7 +79,10 @@ export const ConfirmationModal: React.FC<
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Button onClick={handleClick}>{confirm}</Button>
 | 
					          {/* eslint-disable-next-line jsx-a11y/no-autofocus -- we are in a modal and thus autofocusing is justified */}
 | 
				
			||||||
 | 
					          <Button onClick={handleClick} autoFocus>
 | 
				
			||||||
 | 
					            {confirm}
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -88,7 +88,7 @@ export const DomainBlockModal = ({ domain, accountId, acct }) => {
 | 
				
			||||||
            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
 | 
					            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Button onClick={handleClick}>
 | 
					          <Button onClick={handleClick} autoFocus>
 | 
				
			||||||
            <FormattedMessage id='domain_block_modal.block' defaultMessage='Block server' />
 | 
					            <FormattedMessage id='domain_block_modal.block' defaultMessage='Block server' />
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,101 +0,0 @@
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
 | 
					 | 
				
			||||||
import api from 'flavours/glitch/api';
 | 
					 | 
				
			||||||
import { IconButton } from 'flavours/glitch/components/icon_button';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const messages = defineMessages({
 | 
					 | 
				
			||||||
  close: { id: 'lightbox.close', defaultMessage: 'Close' },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class EmbedModal extends ImmutablePureComponent {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static propTypes = {
 | 
					 | 
				
			||||||
    id: PropTypes.string.isRequired,
 | 
					 | 
				
			||||||
    onClose: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    onError: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    intl: PropTypes.object.isRequired,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  state = {
 | 
					 | 
				
			||||||
    loading: false,
 | 
					 | 
				
			||||||
    oembed: null,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentDidMount () {
 | 
					 | 
				
			||||||
    const { id } = this.props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.setState({ loading: true });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    api().get(`/api/web/embeds/${id}`).then(res => {
 | 
					 | 
				
			||||||
      this.setState({ loading: false, oembed: res.data });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const iframeDocument = this.iframe.contentWindow.document;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iframeDocument.open();
 | 
					 | 
				
			||||||
      iframeDocument.write(res.data.html);
 | 
					 | 
				
			||||||
      iframeDocument.close();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iframeDocument.body.style.margin = 0;
 | 
					 | 
				
			||||||
      this.iframe.width  = iframeDocument.body.scrollWidth;
 | 
					 | 
				
			||||||
      this.iframe.height = iframeDocument.body.scrollHeight;
 | 
					 | 
				
			||||||
    }).catch(error => {
 | 
					 | 
				
			||||||
      this.props.onError(error);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setIframeRef = c =>  {
 | 
					 | 
				
			||||||
    this.iframe = c;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleTextareaClick = (e) => {
 | 
					 | 
				
			||||||
    e.target.select();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					 | 
				
			||||||
    const { intl, onClose } = this.props;
 | 
					 | 
				
			||||||
    const { oembed } = this.state;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <div className='modal-root__modal report-modal embed-modal'>
 | 
					 | 
				
			||||||
        <div className='report-modal__target'>
 | 
					 | 
				
			||||||
          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} size={16} />
 | 
					 | 
				
			||||||
          <FormattedMessage id='status.embed' defaultMessage='Embed' />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div className='report-modal__container embed-modal__container' style={{ display: 'block' }}>
 | 
					 | 
				
			||||||
          <p className='hint'>
 | 
					 | 
				
			||||||
            <FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' />
 | 
					 | 
				
			||||||
          </p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <input
 | 
					 | 
				
			||||||
            type='text'
 | 
					 | 
				
			||||||
            className='embed-modal__html'
 | 
					 | 
				
			||||||
            readOnly
 | 
					 | 
				
			||||||
            value={oembed && oembed.html || ''}
 | 
					 | 
				
			||||||
            onClick={this.handleTextareaClick}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <p className='hint'>
 | 
					 | 
				
			||||||
            <FormattedMessage id='embed.preview' defaultMessage='Here is what it will look like:' />
 | 
					 | 
				
			||||||
          </p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <iframe
 | 
					 | 
				
			||||||
            className='embed-modal__iframe'
 | 
					 | 
				
			||||||
            frameBorder='0'
 | 
					 | 
				
			||||||
            ref={this.setIframeRef}
 | 
					 | 
				
			||||||
            sandbox='allow-scripts allow-same-origin'
 | 
					 | 
				
			||||||
            title='preview'
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default injectIntl(EmbedModal);
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,116 @@
 | 
				
			||||||
 | 
					import { useRef, useState, useEffect } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { showAlertForError } from 'flavours/glitch/actions/alerts';
 | 
				
			||||||
 | 
					import api from 'flavours/glitch/api';
 | 
				
			||||||
 | 
					import { Button } from 'flavours/glitch/components/button';
 | 
				
			||||||
 | 
					import { CopyPasteText } from 'flavours/glitch/components/copy_paste_text';
 | 
				
			||||||
 | 
					import { useAppDispatch } from 'flavours/glitch/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface OEmbedResponse {
 | 
				
			||||||
 | 
					  html: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const EmbedModal: React.FC<{
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  onClose: () => void;
 | 
				
			||||||
 | 
					}> = ({ id, onClose }) => {
 | 
				
			||||||
 | 
					  const iframeRef = useRef<HTMLIFrameElement>(null);
 | 
				
			||||||
 | 
					  const intervalRef = useRef<ReturnType<typeof setInterval>>();
 | 
				
			||||||
 | 
					  const [oembed, setOembed] = useState<OEmbedResponse | null>(null);
 | 
				
			||||||
 | 
					  const dispatch = useAppDispatch();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    api()
 | 
				
			||||||
 | 
					      .get(`/api/web/embeds/${id}`)
 | 
				
			||||||
 | 
					      .then((res) => {
 | 
				
			||||||
 | 
					        const data = res.data as OEmbedResponse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setOembed(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const iframeDocument = iframeRef.current?.contentWindow?.document;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!iframeDocument) {
 | 
				
			||||||
 | 
					          return '';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        iframeDocument.open();
 | 
				
			||||||
 | 
					        iframeDocument.write(data.html);
 | 
				
			||||||
 | 
					        iframeDocument.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        iframeDocument.body.style.margin = '0px';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // This is our best chance to ensure the parent iframe has the correct height...
 | 
				
			||||||
 | 
					        intervalRef.current = setInterval(
 | 
				
			||||||
 | 
					          () =>
 | 
				
			||||||
 | 
					            window.requestAnimationFrame(() => {
 | 
				
			||||||
 | 
					              if (iframeRef.current) {
 | 
				
			||||||
 | 
					                iframeRef.current.width = `${iframeDocument.body.scrollWidth}px`;
 | 
				
			||||||
 | 
					                iframeRef.current.height = `${iframeDocument.body.scrollHeight}px`;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					          100,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return '';
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch((error: unknown) => {
 | 
				
			||||||
 | 
					        dispatch(showAlertForError(error));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }, [dispatch, id, setOembed]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(
 | 
				
			||||||
 | 
					    () => () => {
 | 
				
			||||||
 | 
					      if (intervalRef.current) {
 | 
				
			||||||
 | 
					        clearInterval(intervalRef.current);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className='modal-root__modal dialog-modal'>
 | 
				
			||||||
 | 
					      <div className='dialog-modal__header'>
 | 
				
			||||||
 | 
					        <Button onClick={onClose}>
 | 
				
			||||||
 | 
					          <FormattedMessage id='report.close' defaultMessage='Done' />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					        <span className='dialog-modal__header__title'>
 | 
				
			||||||
 | 
					          <FormattedMessage id='status.embed' defaultMessage='Get embed code' />
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					        <Button secondary onClick={onClose}>
 | 
				
			||||||
 | 
					          <FormattedMessage
 | 
				
			||||||
 | 
					            id='confirmation_modal.cancel'
 | 
				
			||||||
 | 
					            defaultMessage='Cancel'
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className='dialog-modal__content'>
 | 
				
			||||||
 | 
					        <div className='dialog-modal__content__form'>
 | 
				
			||||||
 | 
					          <FormattedMessage
 | 
				
			||||||
 | 
					            id='embed.instructions'
 | 
				
			||||||
 | 
					            defaultMessage='Embed this status on your website by copying the code below.'
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <CopyPasteText value={oembed?.html ?? ''} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <FormattedMessage
 | 
				
			||||||
 | 
					            id='embed.preview'
 | 
				
			||||||
 | 
					            defaultMessage='Here is what it will look like:'
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <iframe
 | 
				
			||||||
 | 
					            frameBorder='0'
 | 
				
			||||||
 | 
					            ref={iframeRef}
 | 
				
			||||||
 | 
					            sandbox='allow-scripts allow-same-origin'
 | 
				
			||||||
 | 
					            title='Preview'
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line import/no-default-export
 | 
				
			||||||
 | 
					export default EmbedModal;
 | 
				
			||||||
| 
						 | 
					@ -137,7 +137,7 @@ export const MuteModal = ({ accountId, acct }) => {
 | 
				
			||||||
            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
 | 
					            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Button onClick={handleClick}>
 | 
					          <Button onClick={handleClick} autoFocus>
 | 
				
			||||||
            <FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
 | 
					            <FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,10 +3,12 @@ import { connect } from 'react-redux';
 | 
				
			||||||
import { openModal, closeModal } from '../../../actions/modal';
 | 
					import { openModal, closeModal } from '../../../actions/modal';
 | 
				
			||||||
import ModalRoot from '../components/modal_root';
 | 
					import ModalRoot from '../components/modal_root';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const defaultProps = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
  ignoreFocus: state.getIn(['modal', 'ignoreFocus']),
 | 
					  ignoreFocus: state.getIn(['modal', 'ignoreFocus']),
 | 
				
			||||||
  type: state.getIn(['modal', 'stack', 0, 'modalType'], null),
 | 
					  type: state.getIn(['modal', 'stack', 0, 'modalType'], null),
 | 
				
			||||||
  props: state.getIn(['modal', 'stack', 0, 'modalProps'], {}),
 | 
					  props: state.getIn(['modal', 'stack', 0, 'modalProps'], defaultProps),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = dispatch => ({
 | 
					const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,24 +4,11 @@ import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { NotificationStack } from 'react-notification';
 | 
					import { NotificationStack } from 'react-notification';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { dismissAlert } from '../../../actions/alerts';
 | 
					import { dismissAlert } from 'flavours/glitch/actions/alerts';
 | 
				
			||||||
import { getAlerts } from '../../../selectors';
 | 
					import { getAlerts } from 'flavours/glitch/selectors';
 | 
				
			||||||
 | 
					 | 
				
			||||||
const formatIfNeeded = (intl, message, values) => {
 | 
					 | 
				
			||||||
  if (typeof message === 'object') {
 | 
					 | 
				
			||||||
    return intl.formatMessage(message, values);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return message;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = (state, { intl }) => ({
 | 
					const mapStateToProps = (state, { intl }) => ({
 | 
				
			||||||
  notifications: getAlerts(state).map(alert => ({
 | 
					  notifications: getAlerts(state, { intl }),
 | 
				
			||||||
    ...alert,
 | 
					 | 
				
			||||||
    action: formatIfNeeded(intl, alert.action, alert.values),
 | 
					 | 
				
			||||||
    title: formatIfNeeded(intl, alert.title, alert.values),
 | 
					 | 
				
			||||||
    message: formatIfNeeded(intl, alert.message, alert.values),
 | 
					 | 
				
			||||||
  })),
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = (dispatch) => ({
 | 
					const mapDispatchToProps = (dispatch) => ({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ const getRegex = createSelector([
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    regex = rawRegex && new RegExp(rawRegex.trim(), 'i');
 | 
					    regex = rawRegex && new RegExp(rawRegex.trim(), 'i');
 | 
				
			||||||
  } catch (e) {
 | 
					  } catch {
 | 
				
			||||||
    // Bad regex, don't affect filters
 | 
					    // Bad regex, don't affect filters
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return regex;
 | 
					  return regex;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -337,8 +337,8 @@ class UI extends PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      e.dataTransfer.dropEffect = 'copy';
 | 
					      e.dataTransfer.dropEffect = 'copy';
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch {
 | 
				
			||||||
 | 
					      // do nothing
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					// This hook allows a component to signal that it's done rendering in a way that
 | 
				
			||||||
 | 
					// can be used by e.g. our embed code to determine correct iframe height
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let renderSignalReceived = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Callback = () => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let onInitialRender: Callback;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const afterInitialRender = (callback: Callback) => {
 | 
				
			||||||
 | 
					  if (renderSignalReceived) {
 | 
				
			||||||
 | 
					    callback();
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    onInitialRender = callback;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useRenderSignal = () => {
 | 
				
			||||||
 | 
					  return () => {
 | 
				
			||||||
 | 
					    if (renderSignalReceived) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    renderSignalReceived = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (typeof onInitialRender !== 'undefined') {
 | 
				
			||||||
 | 
					      window.requestAnimationFrame(() => {
 | 
				
			||||||
 | 
					        onInitialRender();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -90,7 +90,7 @@ if (initialState) {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // @ts-expect-error
 | 
					    // @ts-expect-error
 | 
				
			||||||
    initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
 | 
					    initialState.local_settings = JSON.parse(localStorage.getItem('mastodon-settings'));
 | 
				
			||||||
  } catch (e) {
 | 
					  } catch {
 | 
				
			||||||
    initialState.local_settings = {};
 | 
					    initialState.local_settings = {};
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,7 @@ function onProviderError(error: unknown) {
 | 
				
			||||||
    error &&
 | 
					    error &&
 | 
				
			||||||
    typeof error === 'object' &&
 | 
					    typeof error === 'object' &&
 | 
				
			||||||
    error instanceof Error &&
 | 
					    error instanceof Error &&
 | 
				
			||||||
    error.message.match('MISSING_DATA')
 | 
					    /MISSING_DATA/.exec(error.message)
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    console.warn(error.message);
 | 
					    console.warn(error.message);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,14 +7,16 @@ import { me } from '../initial_state';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export { makeGetAccount } from "./accounts";
 | 
					export { makeGetAccount } from "./accounts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getFilters = (state, { contextType }) => {
 | 
					const getFilters = createSelector([state => state.get('filters'), (_, { contextType }) => contextType], (filters, contextType) => {
 | 
				
			||||||
  if (!contextType) return null;
 | 
					  if (!contextType) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const serverSideType = toServerSideType(contextType);
 | 
					 | 
				
			||||||
  const now = new Date();
 | 
					  const now = new Date();
 | 
				
			||||||
 | 
					  const serverSideType = toServerSideType(contextType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return state.get('filters').filter((filter) => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || filter.get('expires_at') > now));
 | 
					  return filters.filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || filter.get('expires_at') > now));
 | 
				
			||||||
};
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const makeGetStatus = () => {
 | 
					export const makeGetStatus = () => {
 | 
				
			||||||
  return createSelector(
 | 
					  return createSelector(
 | 
				
			||||||
| 
						 | 
					@ -74,10 +76,21 @@ const ALERT_DEFAULTS = {
 | 
				
			||||||
  style: false,
 | 
					  style: false,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getAlerts = createSelector(state => state.get('alerts'), alerts =>
 | 
					const formatIfNeeded = (intl, message, values) => {
 | 
				
			||||||
 | 
					  if (typeof message === 'object') {
 | 
				
			||||||
 | 
					    return intl.formatMessage(message, values);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return message;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getAlerts = createSelector([state => state.get('alerts'), (_, { intl }) => intl], (alerts, intl) =>
 | 
				
			||||||
  alerts.map(item => ({
 | 
					  alerts.map(item => ({
 | 
				
			||||||
    ...ALERT_DEFAULTS,
 | 
					    ...ALERT_DEFAULTS,
 | 
				
			||||||
    ...item,
 | 
					    ...item,
 | 
				
			||||||
 | 
					    action: formatIfNeeded(intl, item.action, item.values),
 | 
				
			||||||
 | 
					    title: formatIfNeeded(intl, item.title, item.values),
 | 
				
			||||||
 | 
					    message: formatIfNeeded(intl, item.message, item.values),
 | 
				
			||||||
  })).toArray());
 | 
					  })).toArray());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const makeGetNotification = () => createSelector([
 | 
					export const makeGetNotification = () => createSelector([
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,7 @@ export default class Settings {
 | 
				
			||||||
      const encodedData = JSON.stringify(data);
 | 
					      const encodedData = JSON.stringify(data);
 | 
				
			||||||
      localStorage.setItem(key, encodedData);
 | 
					      localStorage.setItem(key, encodedData);
 | 
				
			||||||
      return data;
 | 
					      return data;
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch {
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,7 @@ export default class Settings {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const rawData = localStorage.getItem(key);
 | 
					      const rawData = localStorage.getItem(key);
 | 
				
			||||||
      return JSON.parse(rawData);
 | 
					      return JSON.parse(rawData);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch {
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -35,7 +35,8 @@ export default class Settings {
 | 
				
			||||||
      const key = this.generateKey(id);
 | 
					      const key = this.generateKey(id);
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        localStorage.removeItem(key);
 | 
					        localStorage.removeItem(key);
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch {
 | 
				
			||||||
 | 
					        // ignore if the key is not found
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return data;
 | 
					    return data;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,7 +30,7 @@ function isActionWithmaybeAlertParams(
 | 
				
			||||||
  return isAction(action);
 | 
					  return isAction(action);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/ban-types -- we need to use `{}` here to ensure the dispatch types can be merged
 | 
					// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- we need to use `{}` here to ensure the dispatch types can be merged
 | 
				
			||||||
export const errorsMiddleware: Middleware<{}, RootState> =
 | 
					export const errorsMiddleware: Middleware<{}, RootState> =
 | 
				
			||||||
  ({ dispatch }) =>
 | 
					  ({ dispatch }) =>
 | 
				
			||||||
  (next) =>
 | 
					  (next) =>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,7 +51,7 @@ const play = (audio: HTMLAudioElement) => {
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const soundsMiddleware = (): Middleware<
 | 
					export const soundsMiddleware = (): Middleware<
 | 
				
			||||||
  // eslint-disable-next-line @typescript-eslint/ban-types -- we need to use `{}` here to ensure the dispatch types can be merged
 | 
					  // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- we need to use `{}` here to ensure the dispatch types can be merged
 | 
				
			||||||
  {},
 | 
					  {},
 | 
				
			||||||
  RootState
 | 
					  RootState
 | 
				
			||||||
> => {
 | 
					> => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,6 @@
 | 
				
			||||||
@import 'widgets';
 | 
					@import 'widgets';
 | 
				
			||||||
@import 'forms';
 | 
					@import 'forms';
 | 
				
			||||||
@import 'accounts';
 | 
					@import 'accounts';
 | 
				
			||||||
@import 'statuses';
 | 
					 | 
				
			||||||
@import 'components';
 | 
					@import 'components';
 | 
				
			||||||
@import 'polls';
 | 
					@import 'polls';
 | 
				
			||||||
@import 'modal';
 | 
					@import 'modal';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1849,18 +1849,6 @@ body > [data-popper-placement] {
 | 
				
			||||||
  padding: 14px 10px; // glitch: reduced padding
 | 
					  padding: 14px 10px; // glitch: reduced padding
 | 
				
			||||||
  border-top: 1px solid var(--background-border-color);
 | 
					  border-top: 1px solid var(--background-border-color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &--flex {
 | 
					 | 
				
			||||||
    display: flex;
 | 
					 | 
				
			||||||
    flex-wrap: wrap;
 | 
					 | 
				
			||||||
    justify-content: space-between;
 | 
					 | 
				
			||||||
    align-items: flex-start;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .status__content,
 | 
					 | 
				
			||||||
    .detailed-status__meta {
 | 
					 | 
				
			||||||
      flex: 100%;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .status__content {
 | 
					  .status__content {
 | 
				
			||||||
    font-size: 19px;
 | 
					    font-size: 19px;
 | 
				
			||||||
    line-height: 24px;
 | 
					    line-height: 24px;
 | 
				
			||||||
| 
						 | 
					@ -1887,6 +1875,29 @@ body > [data-popper-placement] {
 | 
				
			||||||
    padding: 0;
 | 
					    padding: 0;
 | 
				
			||||||
    margin-bottom: 16px;
 | 
					    margin-bottom: 16px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .logo {
 | 
				
			||||||
 | 
					    width: 40px;
 | 
				
			||||||
 | 
					    height: 40px;
 | 
				
			||||||
 | 
					    color: $dark-text-color;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.embed {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &__overlay {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
 | 
					    left: 0;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .detailed-status {
 | 
				
			||||||
 | 
					    border-top: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.scrollable > div:first-child .detailed-status {
 | 
					.scrollable > div:first-child .detailed-status {
 | 
				
			||||||
| 
						 | 
					@ -4098,7 +4109,7 @@ input.glitch-setting-text {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &__wrapper {
 | 
					  &__wrapper {
 | 
				
			||||||
    background: $white;
 | 
					    background: $white;
 | 
				
			||||||
    border: 1px solid $ui-secondary-color;
 | 
					    border: 1px solid var(--background-border-color);
 | 
				
			||||||
    margin-bottom: 10px;
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
    border-radius: 4px;
 | 
					    border-radius: 4px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6730,6 +6741,50 @@ a.status-card {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dialog-modal {
 | 
				
			||||||
 | 
					  width: 588px;
 | 
				
			||||||
 | 
					  max-height: 80vh;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  background: var(--modal-background-color);
 | 
				
			||||||
 | 
					  backdrop-filter: var(--background-filter);
 | 
				
			||||||
 | 
					  border: 1px solid var(--modal-border-color);
 | 
				
			||||||
 | 
					  border-radius: 16px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &__header {
 | 
				
			||||||
 | 
					    border-bottom: 1px solid var(--modal-border-color);
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    align-items: center;
 | 
				
			||||||
 | 
					    justify-content: space-between;
 | 
				
			||||||
 | 
					    flex-direction: row-reverse;
 | 
				
			||||||
 | 
					    padding: 12px 24px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__title {
 | 
				
			||||||
 | 
					      font-size: 16px;
 | 
				
			||||||
 | 
					      line-height: 24px;
 | 
				
			||||||
 | 
					      font-weight: 500;
 | 
				
			||||||
 | 
					      letter-spacing: 0.15px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &__content {
 | 
				
			||||||
 | 
					    font-size: 14px;
 | 
				
			||||||
 | 
					    line-height: 20px;
 | 
				
			||||||
 | 
					    letter-spacing: 0.25px;
 | 
				
			||||||
 | 
					    overflow-y: auto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__form {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      gap: 16px;
 | 
				
			||||||
 | 
					      padding: 24px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .copy-paste-text {
 | 
				
			||||||
 | 
					    margin-bottom: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.hotkey-combination {
 | 
					.hotkey-combination {
 | 
				
			||||||
  display: inline-flex;
 | 
					  display: inline-flex;
 | 
				
			||||||
  align-items: center;
 | 
					  align-items: center;
 | 
				
			||||||
| 
						 | 
					@ -6747,26 +6802,20 @@ a.status-card {
 | 
				
			||||||
.report-modal,
 | 
					.report-modal,
 | 
				
			||||||
.actions-modal,
 | 
					.actions-modal,
 | 
				
			||||||
.compare-history-modal {
 | 
					.compare-history-modal {
 | 
				
			||||||
  background: lighten($ui-secondary-color, 8%);
 | 
					  background: var(--background-color);
 | 
				
			||||||
  color: $inverted-text-color;
 | 
					  color: $primary-text-color;
 | 
				
			||||||
  border-radius: 8px;
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  border: 1px solid var(--background-border-color);
 | 
				
			||||||
  overflow: hidden;
 | 
					  overflow: hidden;
 | 
				
			||||||
  max-width: 90vw;
 | 
					  max-width: 90vw;
 | 
				
			||||||
  width: 480px;
 | 
					  width: 480px;
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  flex-direction: column;
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .status__relative-time {
 | 
					  @media screen and (max-width: $no-columns-breakpoint) {
 | 
				
			||||||
    order: 2;
 | 
					    border-bottom: 0;
 | 
				
			||||||
 | 
					    border-radius: 4px 4px 0 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  .status__content__spoiler-link {
 | 
					 | 
				
			||||||
    color: lighten($secondary-text-color, 8%);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.boost-modal .status-direct {
 | 
					 | 
				
			||||||
  background-color: inherit;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.boost-modal__container {
 | 
					.boost-modal__container {
 | 
				
			||||||
| 
						 | 
					@ -6806,6 +6855,7 @@ a.status-card {
 | 
				
			||||||
.report-modal {
 | 
					.report-modal {
 | 
				
			||||||
  width: 90vw;
 | 
					  width: 90vw;
 | 
				
			||||||
  max-width: 700px;
 | 
					  max-width: 700px;
 | 
				
			||||||
 | 
					  border: 1px solid var(--background-border-color);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.report-dialog-modal {
 | 
					.report-dialog-modal {
 | 
				
			||||||
| 
						 | 
					@ -7029,7 +7079,7 @@ a.status-card {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.report-modal__container {
 | 
					.report-modal__container {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  border-top: 1px solid $ui-secondary-color;
 | 
					  border-top: 1px solid var(--background-border-color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @media screen and (width <= 480px) {
 | 
					  @media screen and (width <= 480px) {
 | 
				
			||||||
    flex-wrap: wrap;
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
| 
						 | 
					@ -7087,7 +7137,7 @@ a.status-card {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.report-modal__comment {
 | 
					.report-modal__comment {
 | 
				
			||||||
  padding: 20px;
 | 
					  padding: 20px;
 | 
				
			||||||
  border-inline-end: 1px solid $ui-secondary-color;
 | 
					  border-inline-end: 1px solid var(--background-border-color);
 | 
				
			||||||
  max-width: 320px;
 | 
					  max-width: 320px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  p {
 | 
					  p {
 | 
				
			||||||
| 
						 | 
					@ -7098,7 +7148,7 @@ a.status-card {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .setting-text-label {
 | 
					  .setting-text-label {
 | 
				
			||||||
    display: block;
 | 
					    display: block;
 | 
				
			||||||
    color: $inverted-text-color;
 | 
					    color: $secondary-text-color;
 | 
				
			||||||
    font-size: 14px;
 | 
					    font-size: 14px;
 | 
				
			||||||
    font-weight: 500;
 | 
					    font-weight: 500;
 | 
				
			||||||
    margin-bottom: 10px;
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
| 
						 | 
					@ -7164,7 +7214,7 @@ a.status-card {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    li:not(:empty) {
 | 
					    li:not(:empty) {
 | 
				
			||||||
      a {
 | 
					      a {
 | 
				
			||||||
        color: $inverted-text-color;
 | 
					        color: $primary-text-color;
 | 
				
			||||||
        display: flex;
 | 
					        display: flex;
 | 
				
			||||||
        padding: 12px 16px;
 | 
					        padding: 12px 16px;
 | 
				
			||||||
        font-size: 15px;
 | 
					        font-size: 15px;
 | 
				
			||||||
| 
						 | 
					@ -7257,7 +7307,7 @@ a.status-card {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.compare-history-modal {
 | 
					.compare-history-modal {
 | 
				
			||||||
  .report-modal__target {
 | 
					  .report-modal__target {
 | 
				
			||||||
    border-bottom: 1px solid $ui-secondary-color;
 | 
					    border-bottom: 1px solid var(--background-border-color);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &__container {
 | 
					  &__container {
 | 
				
			||||||
| 
						 | 
					@ -7267,7 +7317,7 @@ a.status-card {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .status__content {
 | 
					  .status__content {
 | 
				
			||||||
    color: $inverted-text-color;
 | 
					    color: $secondary-text-color;
 | 
				
			||||||
    font-size: 19px;
 | 
					    font-size: 19px;
 | 
				
			||||||
    line-height: 24px;
 | 
					    line-height: 24px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7357,6 +7407,64 @@ img.modal-warning {
 | 
				
			||||||
  inset-inline-start: 8px;
 | 
					  inset-inline-start: 8px;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  gap: 2px;
 | 
					  gap: 2px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &--layout-2 {
 | 
				
			||||||
 | 
					    .media-gallery__item:nth-child(1) {
 | 
				
			||||||
 | 
					      border-end-end-radius: 0;
 | 
				
			||||||
 | 
					      border-start-end-radius: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .media-gallery__item:nth-child(2) {
 | 
				
			||||||
 | 
					      border-start-start-radius: 0;
 | 
				
			||||||
 | 
					      border-end-start-radius: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &--layout-3 {
 | 
				
			||||||
 | 
					    .media-gallery__item:nth-child(1) {
 | 
				
			||||||
 | 
					      border-end-end-radius: 0;
 | 
				
			||||||
 | 
					      border-start-end-radius: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .media-gallery__item:nth-child(2) {
 | 
				
			||||||
 | 
					      border-start-start-radius: 0;
 | 
				
			||||||
 | 
					      border-end-start-radius: 0;
 | 
				
			||||||
 | 
					      border-end-end-radius: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .media-gallery__item:nth-child(3) {
 | 
				
			||||||
 | 
					      border-start-start-radius: 0;
 | 
				
			||||||
 | 
					      border-end-start-radius: 0;
 | 
				
			||||||
 | 
					      border-start-end-radius: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &--layout-4 {
 | 
				
			||||||
 | 
					    .media-gallery__item:nth-child(1) {
 | 
				
			||||||
 | 
					      border-end-end-radius: 0;
 | 
				
			||||||
 | 
					      border-start-end-radius: 0;
 | 
				
			||||||
 | 
					      border-end-start-radius: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .media-gallery__item:nth-child(2) {
 | 
				
			||||||
 | 
					      border-start-start-radius: 0;
 | 
				
			||||||
 | 
					      border-end-start-radius: 0;
 | 
				
			||||||
 | 
					      border-end-end-radius: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .media-gallery__item:nth-child(3) {
 | 
				
			||||||
 | 
					      border-start-start-radius: 0;
 | 
				
			||||||
 | 
					      border-start-end-radius: 0;
 | 
				
			||||||
 | 
					      border-end-start-radius: 0;
 | 
				
			||||||
 | 
					      border-end-end-radius: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .media-gallery__item:nth-child(4) {
 | 
				
			||||||
 | 
					      border-start-start-radius: 0;
 | 
				
			||||||
 | 
					      border-end-start-radius: 0;
 | 
				
			||||||
 | 
					      border-start-end-radius: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.media-gallery__alt__label,
 | 
					.media-gallery__alt__label,
 | 
				
			||||||
| 
						 | 
					@ -8202,69 +8310,6 @@ noscript {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.embed-modal {
 | 
					 | 
				
			||||||
  width: auto;
 | 
					 | 
				
			||||||
  max-width: 80vw;
 | 
					 | 
				
			||||||
  max-height: 80vh;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  h4 {
 | 
					 | 
				
			||||||
    padding: 30px;
 | 
					 | 
				
			||||||
    font-weight: 500;
 | 
					 | 
				
			||||||
    font-size: 16px;
 | 
					 | 
				
			||||||
    text-align: center;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .embed-modal__container {
 | 
					 | 
				
			||||||
    padding: 10px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .hint {
 | 
					 | 
				
			||||||
      margin-bottom: 15px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .embed-modal__html {
 | 
					 | 
				
			||||||
      outline: 0;
 | 
					 | 
				
			||||||
      box-sizing: border-box;
 | 
					 | 
				
			||||||
      display: block;
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
      border: 0;
 | 
					 | 
				
			||||||
      padding: 10px;
 | 
					 | 
				
			||||||
      font-family: $font-monospace, monospace;
 | 
					 | 
				
			||||||
      background: $ui-base-color;
 | 
					 | 
				
			||||||
      color: $primary-text-color;
 | 
					 | 
				
			||||||
      font-size: 14px;
 | 
					 | 
				
			||||||
      margin: 0;
 | 
					 | 
				
			||||||
      margin-bottom: 15px;
 | 
					 | 
				
			||||||
      border-radius: 4px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &::-moz-focus-inner {
 | 
					 | 
				
			||||||
        border: 0;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &::-moz-focus-inner,
 | 
					 | 
				
			||||||
      &:focus,
 | 
					 | 
				
			||||||
      &:active {
 | 
					 | 
				
			||||||
        outline: 0 !important;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &:focus {
 | 
					 | 
				
			||||||
        background: lighten($ui-base-color, 4%);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      @media screen and (width <= 600px) {
 | 
					 | 
				
			||||||
        font-size: 16px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .embed-modal__iframe {
 | 
					 | 
				
			||||||
      width: 400px;
 | 
					 | 
				
			||||||
      max-width: 100%;
 | 
					 | 
				
			||||||
      overflow: hidden;
 | 
					 | 
				
			||||||
      border: 0;
 | 
					 | 
				
			||||||
      border-radius: 4px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.moved-account-banner,
 | 
					.moved-account-banner,
 | 
				
			||||||
.follow-request-banner,
 | 
					.follow-request-banner,
 | 
				
			||||||
.account-memorial-banner {
 | 
					.account-memorial-banner {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -147,28 +147,6 @@
 | 
				
			||||||
  border-top-color: lighten($ui-base-color, 4%);
 | 
					  border-top-color: lighten($ui-base-color, 4%);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Change the background colors of modals
 | 
					 | 
				
			||||||
.actions-modal,
 | 
					 | 
				
			||||||
.boost-modal,
 | 
					 | 
				
			||||||
.confirmation-modal,
 | 
					 | 
				
			||||||
.mute-modal,
 | 
					 | 
				
			||||||
.block-modal,
 | 
					 | 
				
			||||||
.report-modal,
 | 
					 | 
				
			||||||
.report-dialog-modal,
 | 
					 | 
				
			||||||
.embed-modal,
 | 
					 | 
				
			||||||
.error-modal,
 | 
					 | 
				
			||||||
.onboarding-modal,
 | 
					 | 
				
			||||||
.compare-history-modal,
 | 
					 | 
				
			||||||
.report-modal__comment .setting-text__wrapper,
 | 
					 | 
				
			||||||
.report-modal__comment .setting-text,
 | 
					 | 
				
			||||||
.announcements,
 | 
					 | 
				
			||||||
.picture-in-picture__header,
 | 
					 | 
				
			||||||
.picture-in-picture__footer,
 | 
					 | 
				
			||||||
.reactions-bar__item {
 | 
					 | 
				
			||||||
  background: $white;
 | 
					 | 
				
			||||||
  border: 1px solid var(--background-border-color);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.reactions-bar__item:hover,
 | 
					.reactions-bar__item:hover,
 | 
				
			||||||
.reactions-bar__item:focus,
 | 
					.reactions-bar__item:focus,
 | 
				
			||||||
.reactions-bar__item:active {
 | 
					.reactions-bar__item:active {
 | 
				
			||||||
| 
						 | 
					@ -198,14 +176,6 @@
 | 
				
			||||||
  color: $white;
 | 
					  color: $white;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.report-modal__comment {
 | 
					 | 
				
			||||||
  border-right-color: lighten($ui-base-color, 8%);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.report-modal__container {
 | 
					 | 
				
			||||||
  border-top-color: lighten($ui-base-color, 8%);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.column-settings__hashtags .column-select__option {
 | 
					.column-settings__hashtags .column-select__option {
 | 
				
			||||||
  color: $white;
 | 
					  color: $white;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,239 +0,0 @@
 | 
				
			||||||
.activity-stream {
 | 
					 | 
				
			||||||
  box-shadow: 0 0 15px rgba($base-shadow-color, 0.2);
 | 
					 | 
				
			||||||
  border-radius: 4px;
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  margin-bottom: 10px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @media screen and (max-width: $no-gap-breakpoint) {
 | 
					 | 
				
			||||||
    margin-bottom: 0;
 | 
					 | 
				
			||||||
    border-radius: 0;
 | 
					 | 
				
			||||||
    box-shadow: none;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &--headless {
 | 
					 | 
				
			||||||
    border-radius: 0;
 | 
					 | 
				
			||||||
    margin: 0;
 | 
					 | 
				
			||||||
    box-shadow: none;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .detailed-status,
 | 
					 | 
				
			||||||
    .status {
 | 
					 | 
				
			||||||
      border-radius: 0 !important;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  div[data-component] {
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .entry {
 | 
					 | 
				
			||||||
    background: $ui-base-color;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .detailed-status,
 | 
					 | 
				
			||||||
    .status,
 | 
					 | 
				
			||||||
    .load-more {
 | 
					 | 
				
			||||||
      animation: none;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &:last-child {
 | 
					 | 
				
			||||||
      .detailed-status,
 | 
					 | 
				
			||||||
      .status,
 | 
					 | 
				
			||||||
      .load-more {
 | 
					 | 
				
			||||||
        border-bottom: 0;
 | 
					 | 
				
			||||||
        border-radius: 0 0 4px 4px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &:first-child {
 | 
					 | 
				
			||||||
      .detailed-status,
 | 
					 | 
				
			||||||
      .status,
 | 
					 | 
				
			||||||
      .load-more {
 | 
					 | 
				
			||||||
        border-radius: 4px 4px 0 0;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      &:last-child {
 | 
					 | 
				
			||||||
        .detailed-status,
 | 
					 | 
				
			||||||
        .status,
 | 
					 | 
				
			||||||
        .load-more {
 | 
					 | 
				
			||||||
          border-radius: 4px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    @media screen and (width <= 740px) {
 | 
					 | 
				
			||||||
      .detailed-status,
 | 
					 | 
				
			||||||
      .status,
 | 
					 | 
				
			||||||
      .load-more {
 | 
					 | 
				
			||||||
        border-radius: 0 !important;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &--highlighted .entry {
 | 
					 | 
				
			||||||
    background: lighten($ui-base-color, 8%);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.button.logo-button svg {
 | 
					 | 
				
			||||||
  width: 20px;
 | 
					 | 
				
			||||||
  height: auto;
 | 
					 | 
				
			||||||
  vertical-align: middle;
 | 
					 | 
				
			||||||
  margin-inline-end: 5px;
 | 
					 | 
				
			||||||
  fill: $primary-text-color;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @media screen and (max-width: $no-gap-breakpoint) {
 | 
					 | 
				
			||||||
    display: none;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.embed {
 | 
					 | 
				
			||||||
  .status__content[data-spoiler='folded'] {
 | 
					 | 
				
			||||||
    .e-content {
 | 
					 | 
				
			||||||
      display: none;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    p:first-child {
 | 
					 | 
				
			||||||
      margin-bottom: 0;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .detailed-status {
 | 
					 | 
				
			||||||
    padding: 15px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .detailed-status__display-avatar .account__avatar {
 | 
					 | 
				
			||||||
      width: 48px;
 | 
					 | 
				
			||||||
      height: 48px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  .status {
 | 
					 | 
				
			||||||
    padding: 15px;
 | 
					 | 
				
			||||||
    padding-inline-start: (48px + 15px * 2);
 | 
					 | 
				
			||||||
    min-height: 48px + 2px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &__avatar {
 | 
					 | 
				
			||||||
      inset-inline-start: 15px;
 | 
					 | 
				
			||||||
      top: 17px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .account__avatar {
 | 
					 | 
				
			||||||
        width: 48px;
 | 
					 | 
				
			||||||
        height: 48px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &__content {
 | 
					 | 
				
			||||||
      padding-top: 5px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &__prepend {
 | 
					 | 
				
			||||||
      padding: 8px 0;
 | 
					 | 
				
			||||||
      padding-bottom: 2px;
 | 
					 | 
				
			||||||
      margin: initial;
 | 
					 | 
				
			||||||
      margin-inline-start: 48px + 15px * 2;
 | 
					 | 
				
			||||||
      padding-top: 15px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &__prepend-icon-wrapper {
 | 
					 | 
				
			||||||
      position: absolute;
 | 
					 | 
				
			||||||
      margin: initial;
 | 
					 | 
				
			||||||
      float: initial;
 | 
					 | 
				
			||||||
      width: auto;
 | 
					 | 
				
			||||||
      inset-inline-start: -32px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .media-gallery,
 | 
					 | 
				
			||||||
    &__action-bar,
 | 
					 | 
				
			||||||
    .video-player {
 | 
					 | 
				
			||||||
      margin-top: 10px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    &__action-bar-button {
 | 
					 | 
				
			||||||
      font-size: 18px;
 | 
					 | 
				
			||||||
      width: 23.1429px;
 | 
					 | 
				
			||||||
      height: 23.1429px;
 | 
					 | 
				
			||||||
      line-height: 23.15px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Styling from upstream's WebUI, as public pages use the same layout
 | 
					 | 
				
			||||||
.embed {
 | 
					 | 
				
			||||||
  .status {
 | 
					 | 
				
			||||||
    .status__info {
 | 
					 | 
				
			||||||
      font-size: 15px;
 | 
					 | 
				
			||||||
      display: initial;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .status__relative-time {
 | 
					 | 
				
			||||||
      color: $dark-text-color;
 | 
					 | 
				
			||||||
      float: right;
 | 
					 | 
				
			||||||
      font-size: 14px;
 | 
					 | 
				
			||||||
      width: auto;
 | 
					 | 
				
			||||||
      margin: initial;
 | 
					 | 
				
			||||||
      padding: initial;
 | 
					 | 
				
			||||||
      padding-bottom: 1px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .status__visibility-icon {
 | 
					 | 
				
			||||||
      padding: 0 4px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .icon {
 | 
					 | 
				
			||||||
        width: 1em;
 | 
					 | 
				
			||||||
        height: 1em;
 | 
					 | 
				
			||||||
        margin-bottom: -2px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .status__info .status__display-name {
 | 
					 | 
				
			||||||
      display: block;
 | 
					 | 
				
			||||||
      max-width: 100%;
 | 
					 | 
				
			||||||
      padding: 6px 0;
 | 
					 | 
				
			||||||
      padding-right: 25px;
 | 
					 | 
				
			||||||
      margin: initial;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .status__avatar {
 | 
					 | 
				
			||||||
      height: 48px;
 | 
					 | 
				
			||||||
      position: absolute;
 | 
					 | 
				
			||||||
      width: 48px;
 | 
					 | 
				
			||||||
      margin: initial;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.rtl {
 | 
					 | 
				
			||||||
  .embed {
 | 
					 | 
				
			||||||
    .status {
 | 
					 | 
				
			||||||
      padding-left: 10px;
 | 
					 | 
				
			||||||
      padding-right: 68px;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .status__info .status__display-name {
 | 
					 | 
				
			||||||
        padding-left: 25px;
 | 
					 | 
				
			||||||
        padding-right: 0;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .status__relative-time,
 | 
					 | 
				
			||||||
      .status__visibility-icon {
 | 
					 | 
				
			||||||
        float: left;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status__content__read-more-button,
 | 
					 | 
				
			||||||
.status__content__translate-button {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  font-size: 15px;
 | 
					 | 
				
			||||||
  line-height: 20px;
 | 
					 | 
				
			||||||
  color: $highlight-text-color;
 | 
					 | 
				
			||||||
  border: 0;
 | 
					 | 
				
			||||||
  background: transparent;
 | 
					 | 
				
			||||||
  padding: 0;
 | 
					 | 
				
			||||||
  padding-top: 16px;
 | 
					 | 
				
			||||||
  text-decoration: none;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &:hover,
 | 
					 | 
				
			||||||
  &:active {
 | 
					 | 
				
			||||||
    text-decoration: underline;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@ function _autoUnfoldCW(spoiler_text, skip_unfold_regex) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    regex = new RegExp(skip_unfold_regex.trim(), 'i');
 | 
					    regex = new RegExp(skip_unfold_regex.trim(), 'i');
 | 
				
			||||||
  } catch (e) {
 | 
					  } catch {
 | 
				
			||||||
    // Bad regex, skip filters
 | 
					    // Bad regex, skip filters
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					// This hook allows a component to signal that it's done rendering in a way that
 | 
				
			||||||
 | 
					// can be used by e.g. our embed code to determine correct iframe height
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let renderSignalReceived = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Callback = () => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let onInitialRender: Callback;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const afterInitialRender = (callback: Callback) => {
 | 
				
			||||||
 | 
					  if (renderSignalReceived) {
 | 
				
			||||||
 | 
					    callback();
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    onInitialRender = callback;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const useRenderSignal = () => {
 | 
				
			||||||
 | 
					  return () => {
 | 
				
			||||||
 | 
					    if (renderSignalReceived) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    renderSignalReceived = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (typeof onInitialRender !== 'undefined') {
 | 
				
			||||||
 | 
					      window.requestAnimationFrame(() => {
 | 
				
			||||||
 | 
					        onInitialRender();
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -65,7 +65,7 @@ export const synchronouslySubmitMarkers = createAppAsyncThunk(
 | 
				
			||||||
      client.setRequestHeader('Content-Type', 'application/json');
 | 
					      client.setRequestHeader('Content-Type', 'application/json');
 | 
				
			||||||
      client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
 | 
					      client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
 | 
				
			||||||
      client.send(JSON.stringify(params));
 | 
					      client.send(JSON.stringify(params));
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch {
 | 
				
			||||||
      // Do not make the BeforeUnload handler error out
 | 
					      // Do not make the BeforeUnload handler error out
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,11 +49,13 @@ export function fetchStatusRequest(id, skipLoading) {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function fetchStatus(id, forceFetch = false) {
 | 
					export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
 | 
				
			||||||
  return (dispatch, getState) => {
 | 
					  return (dispatch, getState) => {
 | 
				
			||||||
    const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
 | 
					    const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (alsoFetchContext) {
 | 
				
			||||||
      dispatch(fetchContext(id));
 | 
					      dispatch(fetchContext(id));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (skipLoading) {
 | 
					    if (skipLoading) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -151,7 +151,7 @@ async function refreshHomeTimelineAndNotification(dispatch, getState) {
 | 
				
			||||||
    // TODO: polling for merged notifications
 | 
					    // TODO: polling for merged notifications
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await dispatch(pollRecentGroupNotifications());
 | 
					      await dispatch(pollRecentGroupNotifications());
 | 
				
			||||||
    } catch (error) {
 | 
					    } catch {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ export function start() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    Rails.start();
 | 
					    Rails.start();
 | 
				
			||||||
  } catch (e) {
 | 
					  } catch {
 | 
				
			||||||
    // If called twice
 | 
					    // If called twice
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,90 @@
 | 
				
			||||||
 | 
					import { useRef, useState, useCallback } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
 | 
				
			||||||
 | 
					import { useTimeout } from 'mastodon/../hooks/useTimeout';
 | 
				
			||||||
 | 
					import { Icon } from 'mastodon/components/icon';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
 | 
				
			||||||
 | 
					  const inputRef = useRef<HTMLTextAreaElement>(null);
 | 
				
			||||||
 | 
					  const [copied, setCopied] = useState(false);
 | 
				
			||||||
 | 
					  const [focused, setFocused] = useState(false);
 | 
				
			||||||
 | 
					  const [setAnimationTimeout] = useTimeout();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleInputClick = useCallback(() => {
 | 
				
			||||||
 | 
					    setCopied(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (inputRef.current) {
 | 
				
			||||||
 | 
					      inputRef.current.focus();
 | 
				
			||||||
 | 
					      inputRef.current.select();
 | 
				
			||||||
 | 
					      inputRef.current.setSelectionRange(0, value.length);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [setCopied, value]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleButtonClick = useCallback(
 | 
				
			||||||
 | 
					    (e: React.MouseEvent) => {
 | 
				
			||||||
 | 
					      e.stopPropagation();
 | 
				
			||||||
 | 
					      void navigator.clipboard.writeText(value);
 | 
				
			||||||
 | 
					      inputRef.current?.blur();
 | 
				
			||||||
 | 
					      setCopied(true);
 | 
				
			||||||
 | 
					      setAnimationTimeout(() => {
 | 
				
			||||||
 | 
					        setCopied(false);
 | 
				
			||||||
 | 
					      }, 700);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [setCopied, setAnimationTimeout, value],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleKeyUp = useCallback(
 | 
				
			||||||
 | 
					    (e: React.KeyboardEvent) => {
 | 
				
			||||||
 | 
					      if (e.key !== ' ') return;
 | 
				
			||||||
 | 
					      void navigator.clipboard.writeText(value);
 | 
				
			||||||
 | 
					      setCopied(true);
 | 
				
			||||||
 | 
					      setAnimationTimeout(() => {
 | 
				
			||||||
 | 
					        setCopied(false);
 | 
				
			||||||
 | 
					      }, 700);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [setCopied, setAnimationTimeout, value],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleFocus = useCallback(() => {
 | 
				
			||||||
 | 
					    setFocused(true);
 | 
				
			||||||
 | 
					  }, [setFocused]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleBlur = useCallback(() => {
 | 
				
			||||||
 | 
					    setFocused(false);
 | 
				
			||||||
 | 
					  }, [setFocused]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      className={classNames('copy-paste-text', { copied, focused })}
 | 
				
			||||||
 | 
					      tabIndex={0}
 | 
				
			||||||
 | 
					      role='button'
 | 
				
			||||||
 | 
					      onClick={handleInputClick}
 | 
				
			||||||
 | 
					      onKeyUp={handleKeyUp}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <textarea
 | 
				
			||||||
 | 
					        readOnly
 | 
				
			||||||
 | 
					        value={value}
 | 
				
			||||||
 | 
					        ref={inputRef}
 | 
				
			||||||
 | 
					        onClick={handleInputClick}
 | 
				
			||||||
 | 
					        onFocus={handleFocus}
 | 
				
			||||||
 | 
					        onBlur={handleBlur}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <button className='button' onClick={handleButtonClick}>
 | 
				
			||||||
 | 
					        <Icon id='copy' icon={ContentCopyIcon} />{' '}
 | 
				
			||||||
 | 
					        {copied ? (
 | 
				
			||||||
 | 
					          <FormattedMessage id='copypaste.copied' defaultMessage='Copied' />
 | 
				
			||||||
 | 
					        ) : (
 | 
				
			||||||
 | 
					          <FormattedMessage
 | 
				
			||||||
 | 
					            id='copypaste.copy_to_clipboard'
 | 
				
			||||||
 | 
					            defaultMessage='Copy to clipboard'
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -60,8 +60,8 @@ export default class ErrorBoundary extends PureComponent {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      textarea.select();
 | 
					      textarea.select();
 | 
				
			||||||
      document.execCommand('copy');
 | 
					      document.execCommand('copy');
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch {
 | 
				
			||||||
 | 
					      // do nothing
 | 
				
			||||||
    } finally {
 | 
					    } finally {
 | 
				
			||||||
      document.body.removeChild(textarea);
 | 
					      document.body.removeChild(textarea);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,13 @@ export const WordmarkLogo: React.FC = () => (
 | 
				
			||||||
  </svg>
 | 
					  </svg>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const IconLogo: React.FC = () => (
 | 
				
			||||||
 | 
					  <svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
 | 
				
			||||||
 | 
					    <title>Mastodon</title>
 | 
				
			||||||
 | 
					    <use xlinkHref='#logo-symbol-icon' />
 | 
				
			||||||
 | 
					  </svg>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const SymbolLogo: React.FC = () => (
 | 
					export const SymbolLogo: React.FC = () => (
 | 
				
			||||||
  <img src={logo} alt='Mastodon' className='logo logo--icon' />
 | 
					  <img src={logo} alt='Mastodon' className='logo logo--icon' />
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -327,7 +327,7 @@ class MediaGallery extends PureComponent {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className='media-gallery' style={style} ref={this.handleRef}>
 | 
					      <div className={`media-gallery media-gallery--layout-${size}`} style={style} ref={this.handleRef}>
 | 
				
			||||||
        {(!visible || uncached) && (
 | 
					        {(!visible || uncached) && (
 | 
				
			||||||
          <div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}>
 | 
					          <div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}>
 | 
				
			||||||
            {spoilerButton}
 | 
					            {spoilerButton}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,14 +2,12 @@ import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { IconLogo } from 'mastodon/components/logo';
 | 
				
			||||||
import { AuthorLink } from 'mastodon/features/explore/components/author_link';
 | 
					import { AuthorLink } from 'mastodon/features/explore/components/author_link';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const MoreFromAuthor = ({ accountId }) => (
 | 
					export const MoreFromAuthor = ({ accountId }) => (
 | 
				
			||||||
  <div className='more-from-author'>
 | 
					  <div className='more-from-author'>
 | 
				
			||||||
    <svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
 | 
					    <IconLogo />
 | 
				
			||||||
      <use xlinkHref='#logo-symbol-icon' />
 | 
					 | 
				
			||||||
    </svg>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    <FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
 | 
					    <FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -55,7 +55,7 @@ const messages = defineMessages({
 | 
				
			||||||
  unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
 | 
					  unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
 | 
				
			||||||
  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
 | 
					  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
 | 
				
			||||||
  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
 | 
					  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
 | 
				
			||||||
  embed: { id: 'status.embed', defaultMessage: 'Embed' },
 | 
					  embed: { id: 'status.embed', defaultMessage: 'Get embed code' },
 | 
				
			||||||
  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
 | 
					  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
 | 
				
			||||||
  admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
 | 
					  admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
 | 
				
			||||||
  admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
 | 
					  admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,6 @@ import {
 | 
				
			||||||
  unmuteAccount,
 | 
					  unmuteAccount,
 | 
				
			||||||
  unblockAccount,
 | 
					  unblockAccount,
 | 
				
			||||||
} from '../actions/accounts';
 | 
					} from '../actions/accounts';
 | 
				
			||||||
import { showAlertForError } from '../actions/alerts';
 | 
					 | 
				
			||||||
import { initBlockModal } from '../actions/blocks';
 | 
					import { initBlockModal } from '../actions/blocks';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  replyCompose,
 | 
					  replyCompose,
 | 
				
			||||||
| 
						 | 
					@ -100,10 +99,7 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
 | 
				
			||||||
  onEmbed (status) {
 | 
					  onEmbed (status) {
 | 
				
			||||||
    dispatch(openModal({
 | 
					    dispatch(openModal({
 | 
				
			||||||
      modalType: 'EMBED',
 | 
					      modalType: 'EMBED',
 | 
				
			||||||
      modalProps: {
 | 
					      modalProps: { id: status.get('id') },
 | 
				
			||||||
        id: status.get('id'),
 | 
					 | 
				
			||||||
        onError: error => dispatch(showAlertForError(error)),
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    }));
 | 
					    }));
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -131,7 +131,7 @@ class LoginForm extends React.PureComponent {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      new URL(url);
 | 
					      new URL(url);
 | 
				
			||||||
      return true;
 | 
					      return true;
 | 
				
			||||||
    } catch(_) {
 | 
					    } catch {
 | 
				
			||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,7 @@
 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { isEqual } from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
 | 
					import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
 | 
				
			||||||
import ReplyIcon from '@/material-icons/400-24px/reply-fill.svg?react';
 | 
					import ReplyIcon from '@/material-icons/400-24px/reply-fill.svg?react';
 | 
				
			||||||
import { me } from 'mastodon/initial_state';
 | 
					import { me } from 'mastodon/initial_state';
 | 
				
			||||||
| 
						 | 
					@ -47,7 +49,7 @@ export const NotificationMention: React.FC<{
 | 
				
			||||||
      status.get('visibility') === 'direct',
 | 
					      status.get('visibility') === 'direct',
 | 
				
			||||||
      status.get('in_reply_to_account_id') === me,
 | 
					      status.get('in_reply_to_account_id') === me,
 | 
				
			||||||
    ] as const;
 | 
					    ] as const;
 | 
				
			||||||
  });
 | 
					  }, isEqual);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let labelRenderer = mentionLabelRenderer;
 | 
					  let labelRenderer = mentionLabelRenderer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { Helmet } from 'react-helmet';
 | 
					import { Helmet } from 'react-helmet';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { isEqual } from 'lodash';
 | 
				
			||||||
import { useDebouncedCallback } from 'use-debounce';
 | 
					import { useDebouncedCallback } from 'use-debounce';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
 | 
					import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
 | 
				
			||||||
| 
						 | 
					@ -62,7 +63,7 @@ export const Notifications: React.FC<{
 | 
				
			||||||
  multiColumn?: boolean;
 | 
					  multiColumn?: boolean;
 | 
				
			||||||
}> = ({ columnId, multiColumn }) => {
 | 
					}> = ({ columnId, multiColumn }) => {
 | 
				
			||||||
  const intl = useIntl();
 | 
					  const intl = useIntl();
 | 
				
			||||||
  const notifications = useAppSelector(selectNotificationGroups);
 | 
					  const notifications = useAppSelector(selectNotificationGroups, isEqual);
 | 
				
			||||||
  const dispatch = useAppDispatch();
 | 
					  const dispatch = useAppDispatch();
 | 
				
			||||||
  const isLoading = useAppSelector((s) => s.notificationGroups.isLoading);
 | 
					  const isLoading = useAppSelector((s) => s.notificationGroups.isLoading);
 | 
				
			||||||
  const hasMore = notifications.at(-1)?.type === 'gap';
 | 
					  const hasMore = notifications.at(-1)?.type === 'gap';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,8 +10,8 @@ import { Link } from 'react-router-dom';
 | 
				
			||||||
import SwipeableViews from 'react-swipeable-views';
 | 
					import SwipeableViews from 'react-swipeable-views';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
 | 
					import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
 | 
				
			||||||
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
 | 
					 | 
				
			||||||
import { ColumnBackButton } from 'mastodon/components/column_back_button';
 | 
					import { ColumnBackButton } from 'mastodon/components/column_back_button';
 | 
				
			||||||
 | 
					import { CopyPasteText } from 'mastodon/components/copy_paste_text';
 | 
				
			||||||
import { Icon }  from 'mastodon/components/icon';
 | 
					import { Icon }  from 'mastodon/components/icon';
 | 
				
			||||||
import { me, domain } from 'mastodon/initial_state';
 | 
					import { me, domain } from 'mastodon/initial_state';
 | 
				
			||||||
import { useAppSelector } from 'mastodon/store';
 | 
					import { useAppSelector } from 'mastodon/store';
 | 
				
			||||||
| 
						 | 
					@ -20,67 +20,6 @@ const messages = defineMessages({
 | 
				
			||||||
  shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
 | 
					  shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class CopyPasteText extends PureComponent {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static propTypes = {
 | 
					 | 
				
			||||||
    value: PropTypes.string,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  state = {
 | 
					 | 
				
			||||||
    copied: false,
 | 
					 | 
				
			||||||
    focused: false,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setRef = c => {
 | 
					 | 
				
			||||||
    this.input = c;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleInputClick = () => {
 | 
					 | 
				
			||||||
    this.setState({ copied: false });
 | 
					 | 
				
			||||||
    this.input.focus();
 | 
					 | 
				
			||||||
    this.input.select();
 | 
					 | 
				
			||||||
    this.input.setSelectionRange(0, this.props.value.length);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleButtonClick = e => {
 | 
					 | 
				
			||||||
    e.stopPropagation();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { value } = this.props;
 | 
					 | 
				
			||||||
    navigator.clipboard.writeText(value);
 | 
					 | 
				
			||||||
    this.input.blur();
 | 
					 | 
				
			||||||
    this.setState({ copied: true });
 | 
					 | 
				
			||||||
    this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleFocus = () => {
 | 
					 | 
				
			||||||
    this.setState({ focused: true });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleBlur = () => {
 | 
					 | 
				
			||||||
    this.setState({ focused: false });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentWillUnmount () {
 | 
					 | 
				
			||||||
    if (this.timeout) clearTimeout(this.timeout);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					 | 
				
			||||||
    const { value } = this.props;
 | 
					 | 
				
			||||||
    const { copied, focused } = this.state;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <div className={classNames('copy-paste-text', { copied, focused })} tabIndex='0' role='button' onClick={this.handleInputClick}>
 | 
					 | 
				
			||||||
        <textarea readOnly value={value} ref={this.setRef} onClick={this.handleInputClick} onFocus={this.handleFocus} onBlur={this.handleBlur} />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <button className='button' onClick={this.handleButtonClick}>
 | 
					 | 
				
			||||||
          <Icon id='copy' icon={ContentCopyIcon} /> {copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : <FormattedMessage id='copypaste.copy_to_clipboard' defaultMessage='Copy to clipboard' />}
 | 
					 | 
				
			||||||
        </button>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class TipCarousel extends PureComponent {
 | 
					class TipCarousel extends PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static propTypes = {
 | 
					  static propTypes = {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,87 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/no-unsafe-return,
 | 
				
			||||||
 | 
					                  @typescript-eslint/no-explicit-any,
 | 
				
			||||||
 | 
					                  @typescript-eslint/no-unsafe-assignment */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useEffect, useCallback } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Provider } from 'react-redux';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { useRenderSignal } from 'mastodon/../hooks/useRenderSignal';
 | 
				
			||||||
 | 
					import { fetchStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
 | 
				
			||||||
 | 
					import { hydrateStore } from 'mastodon/actions/store';
 | 
				
			||||||
 | 
					import { Router } from 'mastodon/components/router';
 | 
				
			||||||
 | 
					import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
 | 
				
			||||||
 | 
					import initialState from 'mastodon/initial_state';
 | 
				
			||||||
 | 
					import { IntlProvider } from 'mastodon/locales';
 | 
				
			||||||
 | 
					import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
 | 
				
			||||||
 | 
					import { store, useAppSelector, useAppDispatch } from 'mastodon/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
 | 
				
			||||||
 | 
					const getPictureInPicture = makeGetPictureInPicture() as unknown as (
 | 
				
			||||||
 | 
					  arg0: any,
 | 
				
			||||||
 | 
					  arg1: any,
 | 
				
			||||||
 | 
					) => any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Embed: React.FC<{ id: string }> = ({ id }) => {
 | 
				
			||||||
 | 
					  const status = useAppSelector((state) => getStatus(state, { id }));
 | 
				
			||||||
 | 
					  const pictureInPicture = useAppSelector((state) =>
 | 
				
			||||||
 | 
					    getPictureInPicture(state, { id }),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const domain = useAppSelector((state) => state.meta.get('domain'));
 | 
				
			||||||
 | 
					  const dispatch = useAppDispatch();
 | 
				
			||||||
 | 
					  const dispatchRenderSignal = useRenderSignal();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    dispatch(fetchStatus(id, false, false));
 | 
				
			||||||
 | 
					  }, [dispatch, id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleToggleHidden = useCallback(() => {
 | 
				
			||||||
 | 
					    dispatch(toggleStatusSpoilers(id));
 | 
				
			||||||
 | 
					  }, [dispatch, id]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // This allows us to calculate the correct page height for embeds
 | 
				
			||||||
 | 
					  if (status) {
 | 
				
			||||||
 | 
					    dispatchRenderSignal();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
 | 
				
			||||||
 | 
					  const permalink = status?.get('url') as string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className='embed'>
 | 
				
			||||||
 | 
					      <DetailedStatus
 | 
				
			||||||
 | 
					        status={status}
 | 
				
			||||||
 | 
					        domain={domain}
 | 
				
			||||||
 | 
					        pictureInPicture={pictureInPicture}
 | 
				
			||||||
 | 
					        onToggleHidden={handleToggleHidden}
 | 
				
			||||||
 | 
					        withLogo
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <a
 | 
				
			||||||
 | 
					        className='embed__overlay'
 | 
				
			||||||
 | 
					        href={permalink}
 | 
				
			||||||
 | 
					        target='_blank'
 | 
				
			||||||
 | 
					        rel='noreferrer noopener'
 | 
				
			||||||
 | 
					        aria-label=''
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const Status: React.FC<{ id: string }> = ({ id }) => {
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    if (initialState) {
 | 
				
			||||||
 | 
					      store.dispatch(hydrateStore(initialState));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <IntlProvider>
 | 
				
			||||||
 | 
					      <Provider store={store}>
 | 
				
			||||||
 | 
					        <Router>
 | 
				
			||||||
 | 
					          <Embed id={id} />
 | 
				
			||||||
 | 
					        </Router>
 | 
				
			||||||
 | 
					      </Provider>
 | 
				
			||||||
 | 
					    </IntlProvider>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -49,7 +49,7 @@ const messages = defineMessages({
 | 
				
			||||||
  share: { id: 'status.share', defaultMessage: 'Share' },
 | 
					  share: { id: 'status.share', defaultMessage: 'Share' },
 | 
				
			||||||
  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
 | 
					  pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
 | 
				
			||||||
  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
 | 
					  unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
 | 
				
			||||||
  embed: { id: 'status.embed', defaultMessage: 'Embed' },
 | 
					  embed: { id: 'status.embed', defaultMessage: 'Get embed code' },
 | 
				
			||||||
  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
 | 
					  admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
 | 
				
			||||||
  admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
 | 
					  admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
 | 
				
			||||||
  admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
 | 
					  admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,322 +0,0 @@
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { FormattedDate, FormattedMessage } from 'react-intl';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import classNames from 'classnames';
 | 
					 | 
				
			||||||
import { Link, withRouter } from 'react-router-dom';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
 | 
					 | 
				
			||||||
import { AnimatedNumber } from 'mastodon/components/animated_number';
 | 
					 | 
				
			||||||
import { ContentWarning } from 'mastodon/components/content_warning';
 | 
					 | 
				
			||||||
import EditedTimestamp from 'mastodon/components/edited_timestamp';
 | 
					 | 
				
			||||||
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
 | 
					 | 
				
			||||||
import { Icon }  from 'mastodon/components/icon';
 | 
					 | 
				
			||||||
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
 | 
					 | 
				
			||||||
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
 | 
					 | 
				
			||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { Avatar } from '../../../components/avatar';
 | 
					 | 
				
			||||||
import { DisplayName } from '../../../components/display_name';
 | 
					 | 
				
			||||||
import MediaGallery from '../../../components/media_gallery';
 | 
					 | 
				
			||||||
import StatusContent from '../../../components/status_content';
 | 
					 | 
				
			||||||
import Audio from '../../audio';
 | 
					 | 
				
			||||||
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
 | 
					 | 
				
			||||||
import Video from '../../video';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import Card from './card';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class DetailedStatus extends ImmutablePureComponent {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static propTypes = {
 | 
					 | 
				
			||||||
    status: ImmutablePropTypes.map,
 | 
					 | 
				
			||||||
    onOpenMedia: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    onOpenVideo: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    onToggleHidden: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    onTranslate: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    measureHeight: PropTypes.bool,
 | 
					 | 
				
			||||||
    onHeightChange: PropTypes.func,
 | 
					 | 
				
			||||||
    domain: PropTypes.string.isRequired,
 | 
					 | 
				
			||||||
    compact: PropTypes.bool,
 | 
					 | 
				
			||||||
    showMedia: PropTypes.bool,
 | 
					 | 
				
			||||||
    pictureInPicture: ImmutablePropTypes.contains({
 | 
					 | 
				
			||||||
      inUse: PropTypes.bool,
 | 
					 | 
				
			||||||
      available: PropTypes.bool,
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
    onToggleMediaVisibility: PropTypes.func,
 | 
					 | 
				
			||||||
    ...WithRouterPropTypes,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  state = {
 | 
					 | 
				
			||||||
    height: null,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleAccountClick = (e) => {
 | 
					 | 
				
			||||||
    if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.props.history) {
 | 
					 | 
				
			||||||
      e.preventDefault();
 | 
					 | 
				
			||||||
      this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    e.stopPropagation();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleOpenVideo = (options) => {
 | 
					 | 
				
			||||||
    this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleExpandedToggle = () => {
 | 
					 | 
				
			||||||
    this.props.onToggleHidden(this.props.status);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  _measureHeight (heightJustChanged) {
 | 
					 | 
				
			||||||
    if (this.props.measureHeight && this.node) {
 | 
					 | 
				
			||||||
      scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (this.props.onHeightChange && heightJustChanged) {
 | 
					 | 
				
			||||||
        this.props.onHeightChange();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setRef = c => {
 | 
					 | 
				
			||||||
    this.node = c;
 | 
					 | 
				
			||||||
    this._measureHeight();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentDidUpdate (prevProps, prevState) {
 | 
					 | 
				
			||||||
    this._measureHeight(prevState.height !== this.state.height);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleModalLink = 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');
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleTranslate = () => {
 | 
					 | 
				
			||||||
    const { onTranslate, status } = this.props;
 | 
					 | 
				
			||||||
    onTranslate(status);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  _properStatus () {
 | 
					 | 
				
			||||||
    const { status } = this.props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
 | 
					 | 
				
			||||||
      return status.get('reblog');
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return status;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  getAttachmentAspectRatio () {
 | 
					 | 
				
			||||||
    const attachments = this._properStatus().get('media_attachments');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (attachments.getIn([0, 'type']) === 'video') {
 | 
					 | 
				
			||||||
      return `${attachments.getIn([0, 'meta', 'original', 'width'])} / ${attachments.getIn([0, 'meta', 'original', 'height'])}`;
 | 
					 | 
				
			||||||
    } else if (attachments.getIn([0, 'type']) === 'audio') {
 | 
					 | 
				
			||||||
      return '16 / 9';
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					 | 
				
			||||||
    const status = this._properStatus();
 | 
					 | 
				
			||||||
    const outerStyle = { boxSizing: 'border-box' };
 | 
					 | 
				
			||||||
    const { compact, pictureInPicture } = this.props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!status) {
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let media           = '';
 | 
					 | 
				
			||||||
    let applicationLink = '';
 | 
					 | 
				
			||||||
    let reblogLink = '';
 | 
					 | 
				
			||||||
    let favouriteLink = '';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.props.measureHeight) {
 | 
					 | 
				
			||||||
      outerStyle.height = `${this.state.height}px`;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const language = status.getIn(['translation', 'language']) || status.get('language');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (pictureInPicture.get('inUse')) {
 | 
					 | 
				
			||||||
      media = <PictureInPicturePlaceholder aspectRatio={this.getAttachmentAspectRatio()} />;
 | 
					 | 
				
			||||||
    } else if (status.get('media_attachments').size > 0) {
 | 
					 | 
				
			||||||
      if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
 | 
					 | 
				
			||||||
        const attachment = status.getIn(['media_attachments', 0]);
 | 
					 | 
				
			||||||
        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        media = (
 | 
					 | 
				
			||||||
          <Audio
 | 
					 | 
				
			||||||
            src={attachment.get('url')}
 | 
					 | 
				
			||||||
            alt={description}
 | 
					 | 
				
			||||||
            lang={language}
 | 
					 | 
				
			||||||
            duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
 | 
					 | 
				
			||||||
            poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
 | 
					 | 
				
			||||||
            backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
 | 
					 | 
				
			||||||
            foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
 | 
					 | 
				
			||||||
            accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
 | 
					 | 
				
			||||||
            sensitive={status.get('sensitive')}
 | 
					 | 
				
			||||||
            visible={this.props.showMedia}
 | 
					 | 
				
			||||||
            blurhash={attachment.get('blurhash')}
 | 
					 | 
				
			||||||
            height={150}
 | 
					 | 
				
			||||||
            onToggleVisibility={this.props.onToggleMediaVisibility}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
 | 
					 | 
				
			||||||
        const attachment = status.getIn(['media_attachments', 0]);
 | 
					 | 
				
			||||||
        const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        media = (
 | 
					 | 
				
			||||||
          <Video
 | 
					 | 
				
			||||||
            preview={attachment.get('preview_url')}
 | 
					 | 
				
			||||||
            frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
 | 
					 | 
				
			||||||
            aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`}
 | 
					 | 
				
			||||||
            blurhash={attachment.get('blurhash')}
 | 
					 | 
				
			||||||
            src={attachment.get('url')}
 | 
					 | 
				
			||||||
            alt={description}
 | 
					 | 
				
			||||||
            lang={language}
 | 
					 | 
				
			||||||
            width={300}
 | 
					 | 
				
			||||||
            height={150}
 | 
					 | 
				
			||||||
            onOpenVideo={this.handleOpenVideo}
 | 
					 | 
				
			||||||
            sensitive={status.get('sensitive')}
 | 
					 | 
				
			||||||
            visible={this.props.showMedia}
 | 
					 | 
				
			||||||
            onToggleVisibility={this.props.onToggleMediaVisibility}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        media = (
 | 
					 | 
				
			||||||
          <MediaGallery
 | 
					 | 
				
			||||||
            standalone
 | 
					 | 
				
			||||||
            sensitive={status.get('sensitive')}
 | 
					 | 
				
			||||||
            media={status.get('media_attachments')}
 | 
					 | 
				
			||||||
            lang={language}
 | 
					 | 
				
			||||||
            height={300}
 | 
					 | 
				
			||||||
            onOpenMedia={this.props.onOpenMedia}
 | 
					 | 
				
			||||||
            visible={this.props.showMedia}
 | 
					 | 
				
			||||||
            onToggleVisibility={this.props.onToggleMediaVisibility}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else if (status.get('spoiler_text').length === 0) {
 | 
					 | 
				
			||||||
      media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (status.get('application')) {
 | 
					 | 
				
			||||||
      applicationLink = <>·<a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></>;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const visibilityLink = <>·<VisibilityIcon visibility={status.get('visibility')} /></>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (['private', 'direct'].includes(status.get('visibility'))) {
 | 
					 | 
				
			||||||
      reblogLink = '';
 | 
					 | 
				
			||||||
    } else if (this.props.history) {
 | 
					 | 
				
			||||||
      reblogLink = (
 | 
					 | 
				
			||||||
        <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
 | 
					 | 
				
			||||||
          <span className='detailed-status__reblogs'>
 | 
					 | 
				
			||||||
            <AnimatedNumber value={status.get('reblogs_count')} />
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
          <FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
 | 
					 | 
				
			||||||
        </Link>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      reblogLink = (
 | 
					 | 
				
			||||||
        <a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
 | 
					 | 
				
			||||||
          <span className='detailed-status__reblogs'>
 | 
					 | 
				
			||||||
            <AnimatedNumber value={status.get('reblogs_count')} />
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
          <FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
 | 
					 | 
				
			||||||
        </a>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.props.history) {
 | 
					 | 
				
			||||||
      favouriteLink = (
 | 
					 | 
				
			||||||
        <Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
 | 
					 | 
				
			||||||
          <span className='detailed-status__favorites'>
 | 
					 | 
				
			||||||
            <AnimatedNumber value={status.get('favourites_count')} />
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
          <FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
 | 
					 | 
				
			||||||
        </Link>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      favouriteLink = (
 | 
					 | 
				
			||||||
        <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
 | 
					 | 
				
			||||||
          <span className='detailed-status__favorites'>
 | 
					 | 
				
			||||||
            <AnimatedNumber value={status.get('favourites_count')} />
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
          <FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
 | 
					 | 
				
			||||||
        </a>
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
 | 
					 | 
				
			||||||
    const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <div style={outerStyle}>
 | 
					 | 
				
			||||||
        <div ref={this.setRef} className={classNames('detailed-status', { compact })}>
 | 
					 | 
				
			||||||
          {status.get('visibility') === 'direct' && (
 | 
					 | 
				
			||||||
            <div className='status__prepend'>
 | 
					 | 
				
			||||||
              <div className='status__prepend-icon-wrapper'><Icon id='at' icon={AlternateEmailIcon} className='status__prepend-icon' /></div>
 | 
					 | 
				
			||||||
              <FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' />
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          )}
 | 
					 | 
				
			||||||
          <a href={`/@${status.getIn(['account', 'acct'])}`} data-hover-card-account={status.getIn(['account', 'id'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
 | 
					 | 
				
			||||||
            <div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={46} /></div>
 | 
					 | 
				
			||||||
            <DisplayName account={status.get('account')} localDomain={this.props.domain} />
 | 
					 | 
				
			||||||
          </a>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          {status.get('spoiler_text').length > 0 && <ContentWarning text={status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml')} expanded={expanded} onClick={this.handleExpandedToggle} />}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          {expanded && (
 | 
					 | 
				
			||||||
            <>
 | 
					 | 
				
			||||||
              <StatusContent
 | 
					 | 
				
			||||||
                status={status}
 | 
					 | 
				
			||||||
                onTranslate={this.handleTranslate}
 | 
					 | 
				
			||||||
                {...statusContentProps}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              {media}
 | 
					 | 
				
			||||||
              {hashtagBar}
 | 
					 | 
				
			||||||
            </>
 | 
					 | 
				
			||||||
          )}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <div className='detailed-status__meta'>
 | 
					 | 
				
			||||||
            <div className='detailed-status__meta__line'>
 | 
					 | 
				
			||||||
              <a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
 | 
					 | 
				
			||||||
                <FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
 | 
					 | 
				
			||||||
              </a>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              {visibilityLink}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              {applicationLink}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            {status.get('edited_at') && <div className='detailed-status__meta__line'><EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} /></div>}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            <div className='detailed-status__meta__line'>
 | 
					 | 
				
			||||||
              {reblogLink}
 | 
					 | 
				
			||||||
              {reblogLink && <>·</>}
 | 
					 | 
				
			||||||
              {favouriteLink}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default withRouter(DetailedStatus);
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,390 @@
 | 
				
			||||||
 | 
					/* eslint-disable @typescript-eslint/no-unsafe-member-access,
 | 
				
			||||||
 | 
					                  @typescript-eslint/no-unsafe-call,
 | 
				
			||||||
 | 
					                  @typescript-eslint/no-explicit-any,
 | 
				
			||||||
 | 
					                  @typescript-eslint/no-unsafe-assignment */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import type { CSSProperties } from 'react';
 | 
				
			||||||
 | 
					import { useState, useRef, useCallback } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { FormattedDate, FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					import { Link } from 'react-router-dom';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
 | 
				
			||||||
 | 
					import { AnimatedNumber } from 'mastodon/components/animated_number';
 | 
				
			||||||
 | 
					import { ContentWarning } from 'mastodon/components/content_warning';
 | 
				
			||||||
 | 
					import EditedTimestamp from 'mastodon/components/edited_timestamp';
 | 
				
			||||||
 | 
					import type { StatusLike } from 'mastodon/components/hashtag_bar';
 | 
				
			||||||
 | 
					import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
 | 
				
			||||||
 | 
					import { Icon } from 'mastodon/components/icon';
 | 
				
			||||||
 | 
					import { IconLogo } from 'mastodon/components/logo';
 | 
				
			||||||
 | 
					import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
 | 
				
			||||||
 | 
					import { VisibilityIcon } from 'mastodon/components/visibility_icon';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { Avatar } from '../../../components/avatar';
 | 
				
			||||||
 | 
					import { DisplayName } from '../../../components/display_name';
 | 
				
			||||||
 | 
					import MediaGallery from '../../../components/media_gallery';
 | 
				
			||||||
 | 
					import StatusContent from '../../../components/status_content';
 | 
				
			||||||
 | 
					import Audio from '../../audio';
 | 
				
			||||||
 | 
					import scheduleIdleTask from '../../ui/util/schedule_idle_task';
 | 
				
			||||||
 | 
					import Video from '../../video';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Card from './card';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface VideoModalOptions {
 | 
				
			||||||
 | 
					  startTime: number;
 | 
				
			||||||
 | 
					  autoPlay?: boolean;
 | 
				
			||||||
 | 
					  defaultVolume: number;
 | 
				
			||||||
 | 
					  componentIndex: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DetailedStatus: React.FC<{
 | 
				
			||||||
 | 
					  status: any;
 | 
				
			||||||
 | 
					  onOpenMedia?: (status: any, index: number, lang: string) => void;
 | 
				
			||||||
 | 
					  onOpenVideo?: (status: any, lang: string, options: VideoModalOptions) => void;
 | 
				
			||||||
 | 
					  onTranslate?: (status: any) => void;
 | 
				
			||||||
 | 
					  measureHeight?: boolean;
 | 
				
			||||||
 | 
					  onHeightChange?: () => void;
 | 
				
			||||||
 | 
					  domain: string;
 | 
				
			||||||
 | 
					  showMedia?: boolean;
 | 
				
			||||||
 | 
					  withLogo?: boolean;
 | 
				
			||||||
 | 
					  pictureInPicture: any;
 | 
				
			||||||
 | 
					  onToggleHidden?: (status: any) => void;
 | 
				
			||||||
 | 
					  onToggleMediaVisibility?: () => void;
 | 
				
			||||||
 | 
					}> = ({
 | 
				
			||||||
 | 
					  status,
 | 
				
			||||||
 | 
					  onOpenMedia,
 | 
				
			||||||
 | 
					  onOpenVideo,
 | 
				
			||||||
 | 
					  onTranslate,
 | 
				
			||||||
 | 
					  measureHeight,
 | 
				
			||||||
 | 
					  onHeightChange,
 | 
				
			||||||
 | 
					  domain,
 | 
				
			||||||
 | 
					  showMedia,
 | 
				
			||||||
 | 
					  withLogo,
 | 
				
			||||||
 | 
					  pictureInPicture,
 | 
				
			||||||
 | 
					  onToggleMediaVisibility,
 | 
				
			||||||
 | 
					  onToggleHidden,
 | 
				
			||||||
 | 
					}) => {
 | 
				
			||||||
 | 
					  const properStatus = status?.get('reblog') ?? status;
 | 
				
			||||||
 | 
					  const [height, setHeight] = useState(0);
 | 
				
			||||||
 | 
					  const nodeRef = useRef<HTMLDivElement>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleOpenVideo = useCallback(
 | 
				
			||||||
 | 
					    (options: VideoModalOptions) => {
 | 
				
			||||||
 | 
					      const lang = (status.getIn(['translation', 'language']) ||
 | 
				
			||||||
 | 
					        status.get('language')) as string;
 | 
				
			||||||
 | 
					      if (onOpenVideo)
 | 
				
			||||||
 | 
					        onOpenVideo(status.getIn(['media_attachments', 0]), lang, options);
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [onOpenVideo, status],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleExpandedToggle = useCallback(() => {
 | 
				
			||||||
 | 
					    if (onToggleHidden) onToggleHidden(status);
 | 
				
			||||||
 | 
					  }, [onToggleHidden, status]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const _measureHeight = useCallback(
 | 
				
			||||||
 | 
					    (heightJustChanged?: boolean) => {
 | 
				
			||||||
 | 
					      if (measureHeight && nodeRef.current) {
 | 
				
			||||||
 | 
					        scheduleIdleTask(() => {
 | 
				
			||||||
 | 
					          if (nodeRef.current)
 | 
				
			||||||
 | 
					            setHeight(Math.ceil(nodeRef.current.scrollHeight) + 1);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (onHeightChange && heightJustChanged) {
 | 
				
			||||||
 | 
					          onHeightChange();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [onHeightChange, measureHeight, setHeight],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleRef = useCallback(
 | 
				
			||||||
 | 
					    (c: HTMLDivElement) => {
 | 
				
			||||||
 | 
					      nodeRef.current = c;
 | 
				
			||||||
 | 
					      _measureHeight();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [_measureHeight],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const handleTranslate = useCallback(() => {
 | 
				
			||||||
 | 
					    if (onTranslate) onTranslate(status);
 | 
				
			||||||
 | 
					  }, [onTranslate, status]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!properStatus) {
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let media;
 | 
				
			||||||
 | 
					  let applicationLink;
 | 
				
			||||||
 | 
					  let reblogLink;
 | 
				
			||||||
 | 
					  let attachmentAspectRatio;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (properStatus.get('media_attachments').getIn([0, 'type']) === 'video') {
 | 
				
			||||||
 | 
					    attachmentAspectRatio = `${properStatus.get('media_attachments').getIn([0, 'meta', 'original', 'width'])} / ${properStatus.get('media_attachments').getIn([0, 'meta', 'original', 'height'])}`;
 | 
				
			||||||
 | 
					  } else if (
 | 
				
			||||||
 | 
					    properStatus.get('media_attachments').getIn([0, 'type']) === 'audio'
 | 
				
			||||||
 | 
					  ) {
 | 
				
			||||||
 | 
					    attachmentAspectRatio = '16 / 9';
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    attachmentAspectRatio =
 | 
				
			||||||
 | 
					      properStatus.get('media_attachments').size === 1 &&
 | 
				
			||||||
 | 
					      properStatus
 | 
				
			||||||
 | 
					        .get('media_attachments')
 | 
				
			||||||
 | 
					        .getIn([0, 'meta', 'small', 'aspect'])
 | 
				
			||||||
 | 
					        ? properStatus
 | 
				
			||||||
 | 
					            .get('media_attachments')
 | 
				
			||||||
 | 
					            .getIn([0, 'meta', 'small', 'aspect'])
 | 
				
			||||||
 | 
					        : '3 / 2';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const outerStyle = { boxSizing: 'border-box' } as CSSProperties;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (measureHeight) {
 | 
				
			||||||
 | 
					    outerStyle.height = height;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const language =
 | 
				
			||||||
 | 
					    status.getIn(['translation', 'language']) || status.get('language');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (pictureInPicture.get('inUse')) {
 | 
				
			||||||
 | 
					    media = <PictureInPicturePlaceholder aspectRatio={attachmentAspectRatio} />;
 | 
				
			||||||
 | 
					  } else if (status.get('media_attachments').size > 0) {
 | 
				
			||||||
 | 
					    if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
 | 
				
			||||||
 | 
					      const attachment = status.getIn(['media_attachments', 0]);
 | 
				
			||||||
 | 
					      const description =
 | 
				
			||||||
 | 
					        attachment.getIn(['translation', 'description']) ||
 | 
				
			||||||
 | 
					        attachment.get('description');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      media = (
 | 
				
			||||||
 | 
					        <Audio
 | 
				
			||||||
 | 
					          src={attachment.get('url')}
 | 
				
			||||||
 | 
					          alt={description}
 | 
				
			||||||
 | 
					          lang={language}
 | 
				
			||||||
 | 
					          duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
 | 
				
			||||||
 | 
					          poster={
 | 
				
			||||||
 | 
					            attachment.get('preview_url') ||
 | 
				
			||||||
 | 
					            status.getIn(['account', 'avatar_static'])
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
 | 
				
			||||||
 | 
					          foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
 | 
				
			||||||
 | 
					          accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
 | 
				
			||||||
 | 
					          sensitive={status.get('sensitive')}
 | 
				
			||||||
 | 
					          visible={showMedia}
 | 
				
			||||||
 | 
					          blurhash={attachment.get('blurhash')}
 | 
				
			||||||
 | 
					          height={150}
 | 
				
			||||||
 | 
					          onToggleVisibility={onToggleMediaVisibility}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
 | 
				
			||||||
 | 
					      const attachment = status.getIn(['media_attachments', 0]);
 | 
				
			||||||
 | 
					      const description =
 | 
				
			||||||
 | 
					        attachment.getIn(['translation', 'description']) ||
 | 
				
			||||||
 | 
					        attachment.get('description');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      media = (
 | 
				
			||||||
 | 
					        <Video
 | 
				
			||||||
 | 
					          preview={attachment.get('preview_url')}
 | 
				
			||||||
 | 
					          frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
 | 
				
			||||||
 | 
					          aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`}
 | 
				
			||||||
 | 
					          blurhash={attachment.get('blurhash')}
 | 
				
			||||||
 | 
					          src={attachment.get('url')}
 | 
				
			||||||
 | 
					          alt={description}
 | 
				
			||||||
 | 
					          lang={language}
 | 
				
			||||||
 | 
					          width={300}
 | 
				
			||||||
 | 
					          height={150}
 | 
				
			||||||
 | 
					          onOpenVideo={handleOpenVideo}
 | 
				
			||||||
 | 
					          sensitive={status.get('sensitive')}
 | 
				
			||||||
 | 
					          visible={showMedia}
 | 
				
			||||||
 | 
					          onToggleVisibility={onToggleMediaVisibility}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      media = (
 | 
				
			||||||
 | 
					        <MediaGallery
 | 
				
			||||||
 | 
					          standalone
 | 
				
			||||||
 | 
					          sensitive={status.get('sensitive')}
 | 
				
			||||||
 | 
					          media={status.get('media_attachments')}
 | 
				
			||||||
 | 
					          lang={language}
 | 
				
			||||||
 | 
					          height={300}
 | 
				
			||||||
 | 
					          onOpenMedia={onOpenMedia}
 | 
				
			||||||
 | 
					          visible={showMedia}
 | 
				
			||||||
 | 
					          onToggleVisibility={onToggleMediaVisibility}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } else if (status.get('spoiler_text').length === 0) {
 | 
				
			||||||
 | 
					    media = (
 | 
				
			||||||
 | 
					      <Card
 | 
				
			||||||
 | 
					        sensitive={status.get('sensitive')}
 | 
				
			||||||
 | 
					        onOpenMedia={onOpenMedia}
 | 
				
			||||||
 | 
					        card={status.get('card', null)}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (status.get('application')) {
 | 
				
			||||||
 | 
					    applicationLink = (
 | 
				
			||||||
 | 
					      <>
 | 
				
			||||||
 | 
					        ·
 | 
				
			||||||
 | 
					        <a
 | 
				
			||||||
 | 
					          className='detailed-status__application'
 | 
				
			||||||
 | 
					          href={status.getIn(['application', 'website'])}
 | 
				
			||||||
 | 
					          target='_blank'
 | 
				
			||||||
 | 
					          rel='noopener noreferrer'
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          {status.getIn(['application', 'name'])}
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
 | 
					      </>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const visibilityLink = (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      ·<VisibilityIcon visibility={status.get('visibility')} />
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (['private', 'direct'].includes(status.get('visibility') as string)) {
 | 
				
			||||||
 | 
					    reblogLink = '';
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    reblogLink = (
 | 
				
			||||||
 | 
					      <Link
 | 
				
			||||||
 | 
					        to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`}
 | 
				
			||||||
 | 
					        className='detailed-status__link'
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <span className='detailed-status__reblogs'>
 | 
				
			||||||
 | 
					          <AnimatedNumber value={status.get('reblogs_count')} />
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					        <FormattedMessage
 | 
				
			||||||
 | 
					          id='status.reblogs'
 | 
				
			||||||
 | 
					          defaultMessage='{count, plural, one {boost} other {boosts}}'
 | 
				
			||||||
 | 
					          values={{ count: status.get('reblogs_count') }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </Link>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const favouriteLink = (
 | 
				
			||||||
 | 
					    <Link
 | 
				
			||||||
 | 
					      to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`}
 | 
				
			||||||
 | 
					      className='detailed-status__link'
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <span className='detailed-status__favorites'>
 | 
				
			||||||
 | 
					        <AnimatedNumber value={status.get('favourites_count')} />
 | 
				
			||||||
 | 
					      </span>
 | 
				
			||||||
 | 
					      <FormattedMessage
 | 
				
			||||||
 | 
					        id='status.favourites'
 | 
				
			||||||
 | 
					        defaultMessage='{count, plural, one {favorite} other {favorites}}'
 | 
				
			||||||
 | 
					        values={{ count: status.get('favourites_count') }}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					    </Link>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const { statusContentProps, hashtagBar } = getHashtagBarForStatus(
 | 
				
			||||||
 | 
					    status as StatusLike,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const expanded =
 | 
				
			||||||
 | 
					    !status.get('hidden') || status.get('spoiler_text').length === 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div style={outerStyle}>
 | 
				
			||||||
 | 
					      <div ref={handleRef} className={classNames('detailed-status')}>
 | 
				
			||||||
 | 
					        {status.get('visibility') === 'direct' && (
 | 
				
			||||||
 | 
					          <div className='status__prepend'>
 | 
				
			||||||
 | 
					            <div className='status__prepend-icon-wrapper'>
 | 
				
			||||||
 | 
					              <Icon
 | 
				
			||||||
 | 
					                id='at'
 | 
				
			||||||
 | 
					                icon={AlternateEmailIcon}
 | 
				
			||||||
 | 
					                className='status__prepend-icon'
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <FormattedMessage
 | 
				
			||||||
 | 
					              id='status.direct_indicator'
 | 
				
			||||||
 | 
					              defaultMessage='Private mention'
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        <Link
 | 
				
			||||||
 | 
					          to={`/@${status.getIn(['account', 'acct'])}`}
 | 
				
			||||||
 | 
					          data-hover-card-account={status.getIn(['account', 'id'])}
 | 
				
			||||||
 | 
					          className='detailed-status__display-name'
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <div className='detailed-status__display-avatar'>
 | 
				
			||||||
 | 
					            <Avatar account={status.get('account')} size={46} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <DisplayName account={status.get('account')} localDomain={domain} />
 | 
				
			||||||
 | 
					          {withLogo && (
 | 
				
			||||||
 | 
					            <>
 | 
				
			||||||
 | 
					              <div className='spacer' />
 | 
				
			||||||
 | 
					              <IconLogo />
 | 
				
			||||||
 | 
					            </>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </Link>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {status.get('spoiler_text').length > 0 && (
 | 
				
			||||||
 | 
					          <ContentWarning
 | 
				
			||||||
 | 
					            text={
 | 
				
			||||||
 | 
					              status.getIn(['translation', 'spoilerHtml']) ||
 | 
				
			||||||
 | 
					              status.get('spoilerHtml')
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            expanded={expanded}
 | 
				
			||||||
 | 
					            onClick={handleExpandedToggle}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {expanded && (
 | 
				
			||||||
 | 
					          <>
 | 
				
			||||||
 | 
					            <StatusContent
 | 
				
			||||||
 | 
					              status={status}
 | 
				
			||||||
 | 
					              onTranslate={handleTranslate}
 | 
				
			||||||
 | 
					              {...(statusContentProps as any)}
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {media}
 | 
				
			||||||
 | 
					            {hashtagBar}
 | 
				
			||||||
 | 
					          </>
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className='detailed-status__meta'>
 | 
				
			||||||
 | 
					          <div className='detailed-status__meta__line'>
 | 
				
			||||||
 | 
					            <a
 | 
				
			||||||
 | 
					              className='detailed-status__datetime'
 | 
				
			||||||
 | 
					              href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`}
 | 
				
			||||||
 | 
					              target='_blank'
 | 
				
			||||||
 | 
					              rel='noopener noreferrer'
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <FormattedDate
 | 
				
			||||||
 | 
					                value={new Date(status.get('created_at') as string)}
 | 
				
			||||||
 | 
					                year='numeric'
 | 
				
			||||||
 | 
					                month='short'
 | 
				
			||||||
 | 
					                day='2-digit'
 | 
				
			||||||
 | 
					                hour='2-digit'
 | 
				
			||||||
 | 
					                minute='2-digit'
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </a>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            {visibilityLink}
 | 
				
			||||||
 | 
					            {applicationLink}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          {status.get('edited_at') && (
 | 
				
			||||||
 | 
					            <div className='detailed-status__meta__line'>
 | 
				
			||||||
 | 
					              <EditedTimestamp
 | 
				
			||||||
 | 
					                statusId={status.get('id')}
 | 
				
			||||||
 | 
					                timestamp={status.get('edited_at')}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className='detailed-status__meta__line'>
 | 
				
			||||||
 | 
					            {reblogLink}
 | 
				
			||||||
 | 
					            {reblogLink && <>·</>}
 | 
				
			||||||
 | 
					            {favouriteLink}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -1,140 +0,0 @@
 | 
				
			||||||
import { injectIntl } from 'react-intl';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { showAlertForError } from '../../../actions/alerts';
 | 
					 | 
				
			||||||
import { initBlockModal } from '../../../actions/blocks';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  replyCompose,
 | 
					 | 
				
			||||||
  mentionCompose,
 | 
					 | 
				
			||||||
  directCompose,
 | 
					 | 
				
			||||||
} from '../../../actions/compose';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  toggleReblog,
 | 
					 | 
				
			||||||
  toggleFavourite,
 | 
					 | 
				
			||||||
  pin,
 | 
					 | 
				
			||||||
  unpin,
 | 
					 | 
				
			||||||
} from '../../../actions/interactions';
 | 
					 | 
				
			||||||
import { openModal } from '../../../actions/modal';
 | 
					 | 
				
			||||||
import { initMuteModal } from '../../../actions/mutes';
 | 
					 | 
				
			||||||
import { initReport } from '../../../actions/reports';
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  muteStatus,
 | 
					 | 
				
			||||||
  unmuteStatus,
 | 
					 | 
				
			||||||
  deleteStatus,
 | 
					 | 
				
			||||||
  toggleStatusSpoilers,
 | 
					 | 
				
			||||||
} from '../../../actions/statuses';
 | 
					 | 
				
			||||||
import { deleteModal } from '../../../initial_state';
 | 
					 | 
				
			||||||
import { makeGetStatus, makeGetPictureInPicture } from '../../../selectors';
 | 
					 | 
				
			||||||
import DetailedStatus from '../components/detailed_status';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const makeMapStateToProps = () => {
 | 
					 | 
				
			||||||
  const getStatus = makeGetStatus();
 | 
					 | 
				
			||||||
  const getPictureInPicture = makeGetPictureInPicture();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const mapStateToProps = (state, props) => ({
 | 
					 | 
				
			||||||
    status: getStatus(state, props),
 | 
					 | 
				
			||||||
    domain: state.getIn(['meta', 'domain']),
 | 
					 | 
				
			||||||
    pictureInPicture: getPictureInPicture(state, props),
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return mapStateToProps;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const mapDispatchToProps = (dispatch) => ({
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onReply (status) {
 | 
					 | 
				
			||||||
    dispatch((_, getState) => {
 | 
					 | 
				
			||||||
      let state = getState();
 | 
					 | 
				
			||||||
      if (state.getIn(['compose', 'text']).trim().length !== 0) {
 | 
					 | 
				
			||||||
        dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        dispatch(replyCompose(status));
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onReblog (status, e) {
 | 
					 | 
				
			||||||
    dispatch(toggleReblog(status.get('id'), e.shiftKey));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onFavourite (status) {
 | 
					 | 
				
			||||||
    dispatch(toggleFavourite(status.get('id')));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onPin (status) {
 | 
					 | 
				
			||||||
    if (status.get('pinned')) {
 | 
					 | 
				
			||||||
      dispatch(unpin(status));
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      dispatch(pin(status));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onEmbed (status) {
 | 
					 | 
				
			||||||
    dispatch(openModal({
 | 
					 | 
				
			||||||
      modalType: 'EMBED',
 | 
					 | 
				
			||||||
      modalProps: {
 | 
					 | 
				
			||||||
        id: status.get('id'),
 | 
					 | 
				
			||||||
        onError: error => dispatch(showAlertForError(error)),
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onDelete (status, withRedraft = false) {
 | 
					 | 
				
			||||||
    if (!deleteModal) {
 | 
					 | 
				
			||||||
      dispatch(deleteStatus(status.get('id'), withRedraft));
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onDirect (account) {
 | 
					 | 
				
			||||||
    dispatch(directCompose(account));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onMention (account) {
 | 
					 | 
				
			||||||
    dispatch(mentionCompose(account));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onOpenMedia (media, index, lang) {
 | 
					 | 
				
			||||||
    dispatch(openModal({
 | 
					 | 
				
			||||||
      modalType: 'MEDIA',
 | 
					 | 
				
			||||||
      modalProps: { media, index, lang },
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onOpenVideo (media, lang, options) {
 | 
					 | 
				
			||||||
    dispatch(openModal({
 | 
					 | 
				
			||||||
      modalType: 'VIDEO',
 | 
					 | 
				
			||||||
      modalProps: { media, lang, options },
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onBlock (status) {
 | 
					 | 
				
			||||||
    const account = status.get('account');
 | 
					 | 
				
			||||||
    dispatch(initBlockModal(account));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onReport (status) {
 | 
					 | 
				
			||||||
    dispatch(initReport(status.get('account'), status));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onMute (account) {
 | 
					 | 
				
			||||||
    dispatch(initMuteModal(account));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onMuteConversation (status) {
 | 
					 | 
				
			||||||
    if (status.get('muted')) {
 | 
					 | 
				
			||||||
      dispatch(unmuteStatus(status.get('id')));
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      dispatch(muteStatus(status.get('id')));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onToggleHidden (status) {
 | 
					 | 
				
			||||||
    dispatch(toggleStatusSpoilers(status.get('id')));
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus));
 | 
					 | 
				
			||||||
| 
						 | 
					@ -69,7 +69,7 @@ import Column from '../ui/components/column';
 | 
				
			||||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
 | 
					import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import ActionBar from './components/action_bar';
 | 
					import ActionBar from './components/action_bar';
 | 
				
			||||||
import DetailedStatus from './components/detailed_status';
 | 
					import { DetailedStatus } from './components/detailed_status';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -99,7 +99,7 @@ export const BlockModal = ({ accountId, acct }) => {
 | 
				
			||||||
            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
 | 
					            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Button onClick={handleClick}>
 | 
					          <Button onClick={handleClick} autoFocus>
 | 
				
			||||||
            <FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
 | 
					            <FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -71,7 +71,10 @@ export const ConfirmationModal: React.FC<
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Button onClick={handleClick}>{confirm}</Button>
 | 
					          {/* eslint-disable-next-line jsx-a11y/no-autofocus -- we are in a modal and thus autofocusing is justified */}
 | 
				
			||||||
 | 
					          <Button onClick={handleClick} autoFocus>
 | 
				
			||||||
 | 
					            {confirm}
 | 
				
			||||||
 | 
					          </Button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -88,7 +88,7 @@ export const DomainBlockModal = ({ domain, accountId, acct }) => {
 | 
				
			||||||
            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
 | 
					            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Button onClick={handleClick}>
 | 
					          <Button onClick={handleClick} autoFocus>
 | 
				
			||||||
            <FormattedMessage id='domain_block_modal.block' defaultMessage='Block server' />
 | 
					            <FormattedMessage id='domain_block_modal.block' defaultMessage='Block server' />
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,101 +0,0 @@
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
 | 
					 | 
				
			||||||
import api from 'mastodon/api';
 | 
					 | 
				
			||||||
import { IconButton } from 'mastodon/components/icon_button';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const messages = defineMessages({
 | 
					 | 
				
			||||||
  close: { id: 'lightbox.close', defaultMessage: 'Close' },
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class EmbedModal extends ImmutablePureComponent {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static propTypes = {
 | 
					 | 
				
			||||||
    id: PropTypes.string.isRequired,
 | 
					 | 
				
			||||||
    onClose: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    onError: PropTypes.func.isRequired,
 | 
					 | 
				
			||||||
    intl: PropTypes.object.isRequired,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  state = {
 | 
					 | 
				
			||||||
    loading: false,
 | 
					 | 
				
			||||||
    oembed: null,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  componentDidMount () {
 | 
					 | 
				
			||||||
    const { id } = this.props;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.setState({ loading: true });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    api().get(`/api/web/embeds/${id}`).then(res => {
 | 
					 | 
				
			||||||
      this.setState({ loading: false, oembed: res.data });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const iframeDocument = this.iframe.contentWindow.document;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iframeDocument.open();
 | 
					 | 
				
			||||||
      iframeDocument.write(res.data.html);
 | 
					 | 
				
			||||||
      iframeDocument.close();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      iframeDocument.body.style.margin = 0;
 | 
					 | 
				
			||||||
      this.iframe.width  = iframeDocument.body.scrollWidth;
 | 
					 | 
				
			||||||
      this.iframe.height = iframeDocument.body.scrollHeight;
 | 
					 | 
				
			||||||
    }).catch(error => {
 | 
					 | 
				
			||||||
      this.props.onError(error);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setIframeRef = c =>  {
 | 
					 | 
				
			||||||
    this.iframe = c;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  handleTextareaClick = (e) => {
 | 
					 | 
				
			||||||
    e.target.select();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  render () {
 | 
					 | 
				
			||||||
    const { intl, onClose } = this.props;
 | 
					 | 
				
			||||||
    const { oembed } = this.state;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      <div className='modal-root__modal report-modal embed-modal'>
 | 
					 | 
				
			||||||
        <div className='report-modal__target'>
 | 
					 | 
				
			||||||
          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} size={16} />
 | 
					 | 
				
			||||||
          <FormattedMessage id='status.embed' defaultMessage='Embed' />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div className='report-modal__container embed-modal__container' style={{ display: 'block' }}>
 | 
					 | 
				
			||||||
          <p className='hint'>
 | 
					 | 
				
			||||||
            <FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' />
 | 
					 | 
				
			||||||
          </p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <input
 | 
					 | 
				
			||||||
            type='text'
 | 
					 | 
				
			||||||
            className='embed-modal__html'
 | 
					 | 
				
			||||||
            readOnly
 | 
					 | 
				
			||||||
            value={oembed && oembed.html || ''}
 | 
					 | 
				
			||||||
            onClick={this.handleTextareaClick}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <p className='hint'>
 | 
					 | 
				
			||||||
            <FormattedMessage id='embed.preview' defaultMessage='Here is what it will look like:' />
 | 
					 | 
				
			||||||
          </p>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          <iframe
 | 
					 | 
				
			||||||
            className='embed-modal__iframe'
 | 
					 | 
				
			||||||
            frameBorder='0'
 | 
					 | 
				
			||||||
            ref={this.setIframeRef}
 | 
					 | 
				
			||||||
            sandbox='allow-scripts allow-same-origin'
 | 
					 | 
				
			||||||
            title='preview'
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default injectIntl(EmbedModal);
 | 
					 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,116 @@
 | 
				
			||||||
 | 
					import { useRef, useState, useEffect } from 'react';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { showAlertForError } from 'mastodon/actions/alerts';
 | 
				
			||||||
 | 
					import api from 'mastodon/api';
 | 
				
			||||||
 | 
					import { Button } from 'mastodon/components/button';
 | 
				
			||||||
 | 
					import { CopyPasteText } from 'mastodon/components/copy_paste_text';
 | 
				
			||||||
 | 
					import { useAppDispatch } from 'mastodon/store';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface OEmbedResponse {
 | 
				
			||||||
 | 
					  html: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const EmbedModal: React.FC<{
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  onClose: () => void;
 | 
				
			||||||
 | 
					}> = ({ id, onClose }) => {
 | 
				
			||||||
 | 
					  const iframeRef = useRef<HTMLIFrameElement>(null);
 | 
				
			||||||
 | 
					  const intervalRef = useRef<ReturnType<typeof setInterval>>();
 | 
				
			||||||
 | 
					  const [oembed, setOembed] = useState<OEmbedResponse | null>(null);
 | 
				
			||||||
 | 
					  const dispatch = useAppDispatch();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    api()
 | 
				
			||||||
 | 
					      .get(`/api/web/embeds/${id}`)
 | 
				
			||||||
 | 
					      .then((res) => {
 | 
				
			||||||
 | 
					        const data = res.data as OEmbedResponse;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        setOembed(data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const iframeDocument = iframeRef.current?.contentWindow?.document;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (!iframeDocument) {
 | 
				
			||||||
 | 
					          return '';
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        iframeDocument.open();
 | 
				
			||||||
 | 
					        iframeDocument.write(data.html);
 | 
				
			||||||
 | 
					        iframeDocument.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        iframeDocument.body.style.margin = '0px';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // This is our best chance to ensure the parent iframe has the correct height...
 | 
				
			||||||
 | 
					        intervalRef.current = setInterval(
 | 
				
			||||||
 | 
					          () =>
 | 
				
			||||||
 | 
					            window.requestAnimationFrame(() => {
 | 
				
			||||||
 | 
					              if (iframeRef.current) {
 | 
				
			||||||
 | 
					                iframeRef.current.width = `${iframeDocument.body.scrollWidth}px`;
 | 
				
			||||||
 | 
					                iframeRef.current.height = `${iframeDocument.body.scrollHeight}px`;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					          100,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return '';
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					      .catch((error: unknown) => {
 | 
				
			||||||
 | 
					        dispatch(showAlertForError(error));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					  }, [dispatch, id, setOembed]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(
 | 
				
			||||||
 | 
					    () => () => {
 | 
				
			||||||
 | 
					      if (intervalRef.current) {
 | 
				
			||||||
 | 
					        clearInterval(intervalRef.current);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    [],
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div className='modal-root__modal dialog-modal'>
 | 
				
			||||||
 | 
					      <div className='dialog-modal__header'>
 | 
				
			||||||
 | 
					        <Button onClick={onClose}>
 | 
				
			||||||
 | 
					          <FormattedMessage id='report.close' defaultMessage='Done' />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					        <span className='dialog-modal__header__title'>
 | 
				
			||||||
 | 
					          <FormattedMessage id='status.embed' defaultMessage='Get embed code' />
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					        <Button secondary onClick={onClose}>
 | 
				
			||||||
 | 
					          <FormattedMessage
 | 
				
			||||||
 | 
					            id='confirmation_modal.cancel'
 | 
				
			||||||
 | 
					            defaultMessage='Cancel'
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div className='dialog-modal__content'>
 | 
				
			||||||
 | 
					        <div className='dialog-modal__content__form'>
 | 
				
			||||||
 | 
					          <FormattedMessage
 | 
				
			||||||
 | 
					            id='embed.instructions'
 | 
				
			||||||
 | 
					            defaultMessage='Embed this status on your website by copying the code below.'
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <CopyPasteText value={oembed?.html ?? ''} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <FormattedMessage
 | 
				
			||||||
 | 
					            id='embed.preview'
 | 
				
			||||||
 | 
					            defaultMessage='Here is what it will look like:'
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <iframe
 | 
				
			||||||
 | 
					            frameBorder='0'
 | 
				
			||||||
 | 
					            ref={iframeRef}
 | 
				
			||||||
 | 
					            sandbox='allow-scripts allow-same-origin'
 | 
				
			||||||
 | 
					            title='Preview'
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// eslint-disable-next-line import/no-default-export
 | 
				
			||||||
 | 
					export default EmbedModal;
 | 
				
			||||||
| 
						 | 
					@ -137,7 +137,7 @@ export const MuteModal = ({ accountId, acct }) => {
 | 
				
			||||||
            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
 | 
					            <FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Button onClick={handleClick}>
 | 
					          <Button onClick={handleClick} autoFocus>
 | 
				
			||||||
            <FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
 | 
					            <FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
 | 
				
			||||||
          </Button>
 | 
					          </Button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,10 +3,12 @@ import { connect } from 'react-redux';
 | 
				
			||||||
import { openModal, closeModal } from '../../../actions/modal';
 | 
					import { openModal, closeModal } from '../../../actions/modal';
 | 
				
			||||||
import ModalRoot from '../components/modal_root';
 | 
					import ModalRoot from '../components/modal_root';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const defaultProps = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
  ignoreFocus: state.getIn(['modal', 'ignoreFocus']),
 | 
					  ignoreFocus: state.getIn(['modal', 'ignoreFocus']),
 | 
				
			||||||
  type: state.getIn(['modal', 'stack', 0, 'modalType'], null),
 | 
					  type: state.getIn(['modal', 'stack', 0, 'modalType'], null),
 | 
				
			||||||
  props: state.getIn(['modal', 'stack', 0, 'modalProps'], {}),
 | 
					  props: state.getIn(['modal', 'stack', 0, 'modalProps'], defaultProps),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = dispatch => ({
 | 
					const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,24 +4,11 @@ import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { NotificationStack } from 'react-notification';
 | 
					import { NotificationStack } from 'react-notification';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { dismissAlert } from '../../../actions/alerts';
 | 
					import { dismissAlert } from 'mastodon/actions/alerts';
 | 
				
			||||||
import { getAlerts } from '../../../selectors';
 | 
					import { getAlerts } from 'mastodon/selectors';
 | 
				
			||||||
 | 
					 | 
				
			||||||
const formatIfNeeded = (intl, message, values) => {
 | 
					 | 
				
			||||||
  if (typeof message === 'object') {
 | 
					 | 
				
			||||||
    return intl.formatMessage(message, values);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return message;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = (state, { intl }) => ({
 | 
					const mapStateToProps = (state, { intl }) => ({
 | 
				
			||||||
  notifications: getAlerts(state).map(alert => ({
 | 
					  notifications: getAlerts(state, { intl }),
 | 
				
			||||||
    ...alert,
 | 
					 | 
				
			||||||
    action: formatIfNeeded(intl, alert.action, alert.values),
 | 
					 | 
				
			||||||
    title: formatIfNeeded(intl, alert.title, alert.values),
 | 
					 | 
				
			||||||
    message: formatIfNeeded(intl, alert.message, alert.values),
 | 
					 | 
				
			||||||
  })),
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = (dispatch) => ({
 | 
					const mapDispatchToProps = (dispatch) => ({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -319,8 +319,8 @@ class UI extends PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      e.dataTransfer.dropEffect = 'copy';
 | 
					      e.dataTransfer.dropEffect = 'copy';
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch {
 | 
				
			||||||
 | 
					      // do nothing
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -308,7 +308,6 @@
 | 
				
			||||||
  "lists.search": "Buscar entre la chent a la quala sigues",
 | 
					  "lists.search": "Buscar entre la chent a la quala sigues",
 | 
				
			||||||
  "lists.subheading": "Las tuyas listas",
 | 
					  "lists.subheading": "Las tuyas listas",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
 | 
					  "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
 | 
				
			||||||
  "media_gallery.toggle_visible": "{number, plural, one {Amaga la imachen} other {Amaga las imáchens}}",
 | 
					 | 
				
			||||||
  "moved_to_account_banner.text": "La tuya cuenta {disabledAccount} ye actualment deshabilitada perque t'has mudau a {movedToAccount}.",
 | 
					  "moved_to_account_banner.text": "La tuya cuenta {disabledAccount} ye actualment deshabilitada perque t'has mudau a {movedToAccount}.",
 | 
				
			||||||
  "navigation_bar.about": "Sobre",
 | 
					  "navigation_bar.about": "Sobre",
 | 
				
			||||||
  "navigation_bar.blocks": "Usuarios blocaus",
 | 
					  "navigation_bar.blocks": "Usuarios blocaus",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -443,7 +443,6 @@
 | 
				
			||||||
  "lists.subheading": "قوائمك",
 | 
					  "lists.subheading": "قوائمك",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# عنصر جديد} other {# عناصر جديدة}}",
 | 
					  "load_pending": "{count, plural, one {# عنصر جديد} other {# عناصر جديدة}}",
 | 
				
			||||||
  "loading_indicator.label": "جاري التحميل…",
 | 
					  "loading_indicator.label": "جاري التحميل…",
 | 
				
			||||||
  "media_gallery.toggle_visible": "{number, plural, zero {} one {اخف الصورة} two {اخف الصورتين} few {اخف الصور} many {اخف الصور} other {اخف الصور}}",
 | 
					 | 
				
			||||||
  "moved_to_account_banner.text": "حسابك {disabledAccount} معطل حاليًا لأنك انتقلت إلى {movedToAccount}.",
 | 
					  "moved_to_account_banner.text": "حسابك {disabledAccount} معطل حاليًا لأنك انتقلت إلى {movedToAccount}.",
 | 
				
			||||||
  "mute_modal.hide_from_notifications": "إخفاء من قائمة الإشعارات",
 | 
					  "mute_modal.hide_from_notifications": "إخفاء من قائمة الإشعارات",
 | 
				
			||||||
  "mute_modal.hide_options": "إخفاء الخيارات",
 | 
					  "mute_modal.hide_options": "إخفاء الخيارات",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -268,7 +268,6 @@
 | 
				
			||||||
  "lists.search": "Buscar ente los perfiles que sigues",
 | 
					  "lists.search": "Buscar ente los perfiles que sigues",
 | 
				
			||||||
  "lists.subheading": "Les tos llistes",
 | 
					  "lists.subheading": "Les tos llistes",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# elementu nuevu} other {# elementos nuevos}}",
 | 
					  "load_pending": "{count, plural, one {# elementu nuevu} other {# elementos nuevos}}",
 | 
				
			||||||
  "media_gallery.toggle_visible": "{number, plural, one {Anubrir la imaxe} other {Anubrir les imáxenes}}",
 | 
					 | 
				
			||||||
  "navigation_bar.about": "Tocante a",
 | 
					  "navigation_bar.about": "Tocante a",
 | 
				
			||||||
  "navigation_bar.blocks": "Perfiles bloquiaos",
 | 
					  "navigation_bar.blocks": "Perfiles bloquiaos",
 | 
				
			||||||
  "navigation_bar.bookmarks": "Marcadores",
 | 
					  "navigation_bar.bookmarks": "Marcadores",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -437,7 +437,6 @@
 | 
				
			||||||
  "lists.subheading": "Вашыя спісы",
 | 
					  "lists.subheading": "Вашыя спісы",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# новы элемент} few {# новыя элементы} many {# новых элементаў} other {# новых элементаў}}",
 | 
					  "load_pending": "{count, plural, one {# новы элемент} few {# новыя элементы} many {# новых элементаў} other {# новых элементаў}}",
 | 
				
			||||||
  "loading_indicator.label": "Загрузка…",
 | 
					  "loading_indicator.label": "Загрузка…",
 | 
				
			||||||
  "media_gallery.toggle_visible": "{number, plural, one {Схаваць відарыс} other {Схаваць відарысы}}",
 | 
					 | 
				
			||||||
  "moved_to_account_banner.text": "Ваш уліковы запіс {disabledAccount} зараз адключаны таму што вы перанесены на {movedToAccount}.",
 | 
					  "moved_to_account_banner.text": "Ваш уліковы запіс {disabledAccount} зараз адключаны таму што вы перанесены на {movedToAccount}.",
 | 
				
			||||||
  "mute_modal.hide_from_notifications": "Схаваць з апавяшчэнняў",
 | 
					  "mute_modal.hide_from_notifications": "Схаваць з апавяшчэнняў",
 | 
				
			||||||
  "mute_modal.hide_options": "Схаваць опцыі",
 | 
					  "mute_modal.hide_options": "Схаваць опцыі",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -444,7 +444,6 @@
 | 
				
			||||||
  "lists.subheading": "Вашите списъци",
 | 
					  "lists.subheading": "Вашите списъци",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# нов елемент} other {# нови елемента}}",
 | 
					  "load_pending": "{count, plural, one {# нов елемент} other {# нови елемента}}",
 | 
				
			||||||
  "loading_indicator.label": "Зареждане…",
 | 
					  "loading_indicator.label": "Зареждане…",
 | 
				
			||||||
  "media_gallery.toggle_visible": "Скриване на {number, plural, one {изображение} other {изображения}}",
 | 
					 | 
				
			||||||
  "moved_to_account_banner.text": "Вашият акаунт {disabledAccount} сега е изключен, защото се преместихте в {movedToAccount}.",
 | 
					  "moved_to_account_banner.text": "Вашият акаунт {disabledAccount} сега е изключен, защото се преместихте в {movedToAccount}.",
 | 
				
			||||||
  "mute_modal.hide_from_notifications": "Скриване от известията",
 | 
					  "mute_modal.hide_from_notifications": "Скриване от известията",
 | 
				
			||||||
  "mute_modal.hide_options": "Скриване на възможностите",
 | 
					  "mute_modal.hide_options": "Скриване на възможностите",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -288,7 +288,6 @@
 | 
				
			||||||
  "lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন",
 | 
					  "lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন",
 | 
				
			||||||
  "lists.subheading": "আপনার তালিকা",
 | 
					  "lists.subheading": "আপনার তালিকা",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# নতুন জিনিস} other {# নতুন জিনিস}}",
 | 
					  "load_pending": "{count, plural, one {# নতুন জিনিস} other {# নতুন জিনিস}}",
 | 
				
			||||||
  "media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান",
 | 
					 | 
				
			||||||
  "navigation_bar.about": "পরিচিতি",
 | 
					  "navigation_bar.about": "পরিচিতি",
 | 
				
			||||||
  "navigation_bar.blocks": "বন্ধ করা ব্যবহারকারী",
 | 
					  "navigation_bar.blocks": "বন্ধ করা ব্যবহারকারী",
 | 
				
			||||||
  "navigation_bar.bookmarks": "বুকমার্ক",
 | 
					  "navigation_bar.bookmarks": "বুকমার্ক",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -360,7 +360,6 @@
 | 
				
			||||||
  "lists.subheading": "Ho listennoù",
 | 
					  "lists.subheading": "Ho listennoù",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# dra nevez} other {# dra nevez}}",
 | 
					  "load_pending": "{count, plural, one {# dra nevez} other {# dra nevez}}",
 | 
				
			||||||
  "loading_indicator.label": "O kargañ…",
 | 
					  "loading_indicator.label": "O kargañ…",
 | 
				
			||||||
  "media_gallery.toggle_visible": "{number, plural, one {Kuzhat ar skeudenn} other {Kuzhat ar skeudenn}}",
 | 
					 | 
				
			||||||
  "navigation_bar.about": "Diwar-benn",
 | 
					  "navigation_bar.about": "Diwar-benn",
 | 
				
			||||||
  "navigation_bar.blocks": "Implijer·ezed·ien berzet",
 | 
					  "navigation_bar.blocks": "Implijer·ezed·ien berzet",
 | 
				
			||||||
  "navigation_bar.bookmarks": "Sinedoù",
 | 
					  "navigation_bar.bookmarks": "Sinedoù",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -457,7 +457,7 @@
 | 
				
			||||||
  "lists.subheading": "Les teves llistes",
 | 
					  "lists.subheading": "Les teves llistes",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# element nou} other {# elements nous}}",
 | 
					  "load_pending": "{count, plural, one {# element nou} other {# elements nous}}",
 | 
				
			||||||
  "loading_indicator.label": "Es carrega…",
 | 
					  "loading_indicator.label": "Es carrega…",
 | 
				
			||||||
  "media_gallery.toggle_visible": "{number, plural, one {Amaga la imatge} other {Amaga les imatges}}",
 | 
					  "media_gallery.hide": "Amaga",
 | 
				
			||||||
  "moved_to_account_banner.text": "El teu compte {disabledAccount} està desactivat perquè l'has mogut a {movedToAccount}.",
 | 
					  "moved_to_account_banner.text": "El teu compte {disabledAccount} està desactivat perquè l'has mogut a {movedToAccount}.",
 | 
				
			||||||
  "mute_modal.hide_from_notifications": "Amaga de les notificacions",
 | 
					  "mute_modal.hide_from_notifications": "Amaga de les notificacions",
 | 
				
			||||||
  "mute_modal.hide_options": "Amaga les opcions",
 | 
					  "mute_modal.hide_options": "Amaga les opcions",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -355,7 +355,6 @@
 | 
				
			||||||
  "lists.search": "بگەڕێ لەناو ئەو کەسانەی کە شوێنیان کەوتویت",
 | 
					  "lists.search": "بگەڕێ لەناو ئەو کەسانەی کە شوێنیان کەوتویت",
 | 
				
			||||||
  "lists.subheading": "لیستەکانت",
 | 
					  "lists.subheading": "لیستەکانت",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# بەڕگەی نوێ} other {# بەڕگەی نوێ}}",
 | 
					  "load_pending": "{count, plural, one {# بەڕگەی نوێ} other {# بەڕگەی نوێ}}",
 | 
				
			||||||
  "media_gallery.toggle_visible": "شاردنەوەی {number, plural, one {image} other {images}}",
 | 
					 | 
				
			||||||
  "moved_to_account_banner.text": "ئەکاونتەکەت {disabledAccount} لە ئێستادا لەکارخراوە چونکە تۆ چوویتە {movedToAccount}.",
 | 
					  "moved_to_account_banner.text": "ئەکاونتەکەت {disabledAccount} لە ئێستادا لەکارخراوە چونکە تۆ چوویتە {movedToAccount}.",
 | 
				
			||||||
  "navigation_bar.about": "دەربارە",
 | 
					  "navigation_bar.about": "دەربارە",
 | 
				
			||||||
  "navigation_bar.blocks": "بەکارهێنەرە بلۆککراوەکان",
 | 
					  "navigation_bar.blocks": "بەکارهێنەرە بلۆککراوەکان",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -214,7 +214,6 @@
 | 
				
			||||||
  "lists.search": "Circà indè i vostr'abbunamenti",
 | 
					  "lists.search": "Circà indè i vostr'abbunamenti",
 | 
				
			||||||
  "lists.subheading": "E vo liste",
 | 
					  "lists.subheading": "E vo liste",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# entrata nova} other {# entrate nove}}",
 | 
					  "load_pending": "{count, plural, one {# entrata nova} other {# entrate nove}}",
 | 
				
			||||||
  "media_gallery.toggle_visible": "Piattà {number, plural, one {ritrattu} other {ritratti}}",
 | 
					 | 
				
			||||||
  "navigation_bar.blocks": "Utilizatori bluccati",
 | 
					  "navigation_bar.blocks": "Utilizatori bluccati",
 | 
				
			||||||
  "navigation_bar.bookmarks": "Segnalibri",
 | 
					  "navigation_bar.bookmarks": "Segnalibri",
 | 
				
			||||||
  "navigation_bar.community_timeline": "Linea pubblica lucale",
 | 
					  "navigation_bar.community_timeline": "Linea pubblica lucale",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -435,7 +435,6 @@
 | 
				
			||||||
  "lists.subheading": "Vaše seznamy",
 | 
					  "lists.subheading": "Vaše seznamy",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položek} other {# nových položek}}",
 | 
					  "load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položek} other {# nových položek}}",
 | 
				
			||||||
  "loading_indicator.label": "Načítání…",
 | 
					  "loading_indicator.label": "Načítání…",
 | 
				
			||||||
  "media_gallery.toggle_visible": "{number, plural, one {Skrýt obrázek} few {Skrýt obrázky} many {Skrýt obrázky} other {Skrýt obrázky}}",
 | 
					 | 
				
			||||||
  "moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.",
 | 
					  "moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.",
 | 
				
			||||||
  "mute_modal.hide_from_notifications": "Skrýt z notifikací",
 | 
					  "mute_modal.hide_from_notifications": "Skrýt z notifikací",
 | 
				
			||||||
  "mute_modal.hide_options": "Skrýt možnosti",
 | 
					  "mute_modal.hide_options": "Skrýt možnosti",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -97,7 +97,7 @@
 | 
				
			||||||
  "block_modal.title": "Blocio defnyddiwr?",
 | 
					  "block_modal.title": "Blocio defnyddiwr?",
 | 
				
			||||||
  "block_modal.you_wont_see_mentions": "Fyddwch chi ddim yn gweld postiadau sy'n sôn amdanyn nhw.",
 | 
					  "block_modal.you_wont_see_mentions": "Fyddwch chi ddim yn gweld postiadau sy'n sôn amdanyn nhw.",
 | 
				
			||||||
  "boost_modal.combo": "Mae modd pwyso {combo} er mwyn hepgor hyn tro nesa",
 | 
					  "boost_modal.combo": "Mae modd pwyso {combo} er mwyn hepgor hyn tro nesa",
 | 
				
			||||||
  "boost_modal.reblog": "Hybu postiad",
 | 
					  "boost_modal.reblog": "Hybu postiad?",
 | 
				
			||||||
  "boost_modal.undo_reblog": "Dad-hybu postiad?",
 | 
					  "boost_modal.undo_reblog": "Dad-hybu postiad?",
 | 
				
			||||||
  "bundle_column_error.copy_stacktrace": "Copïo'r adroddiad gwall",
 | 
					  "bundle_column_error.copy_stacktrace": "Copïo'r adroddiad gwall",
 | 
				
			||||||
  "bundle_column_error.error.body": "Nid oedd modd cynhyrchu'r dudalen honno. Gall fod oherwydd gwall yn ein cod neu fater cydnawsedd porwr.",
 | 
					  "bundle_column_error.error.body": "Nid oedd modd cynhyrchu'r dudalen honno. Gall fod oherwydd gwall yn ein cod neu fater cydnawsedd porwr.",
 | 
				
			||||||
| 
						 | 
					@ -457,7 +457,7 @@
 | 
				
			||||||
  "lists.subheading": "Eich rhestrau",
 | 
					  "lists.subheading": "Eich rhestrau",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}",
 | 
					  "load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}",
 | 
				
			||||||
  "loading_indicator.label": "Yn llwytho…",
 | 
					  "loading_indicator.label": "Yn llwytho…",
 | 
				
			||||||
  "media_gallery.toggle_visible": "{number, plural, one {Cuddio delwedd} other {Cuddio delwedd}}",
 | 
					  "media_gallery.hide": "Cuddio",
 | 
				
			||||||
  "moved_to_account_banner.text": "Ar hyn y bryd, mae eich cyfrif {disabledAccount} wedi ei analluogi am i chi symud i {movedToAccount}.",
 | 
					  "moved_to_account_banner.text": "Ar hyn y bryd, mae eich cyfrif {disabledAccount} wedi ei analluogi am i chi symud i {movedToAccount}.",
 | 
				
			||||||
  "mute_modal.hide_from_notifications": "Cuddio rhag hysbysiadau",
 | 
					  "mute_modal.hide_from_notifications": "Cuddio rhag hysbysiadau",
 | 
				
			||||||
  "mute_modal.hide_options": "Cuddio'r dewis",
 | 
					  "mute_modal.hide_options": "Cuddio'r dewis",
 | 
				
			||||||
| 
						 | 
					@ -780,6 +780,7 @@
 | 
				
			||||||
  "status.bookmark": "Llyfrnodi",
 | 
					  "status.bookmark": "Llyfrnodi",
 | 
				
			||||||
  "status.cancel_reblog_private": "Dadhybu",
 | 
					  "status.cancel_reblog_private": "Dadhybu",
 | 
				
			||||||
  "status.cannot_reblog": "Nid oes modd hybu'r postiad hwn",
 | 
					  "status.cannot_reblog": "Nid oes modd hybu'r postiad hwn",
 | 
				
			||||||
 | 
					  "status.continued_thread": "Edefyn parhaus",
 | 
				
			||||||
  "status.copy": "Copïo dolen i'r post",
 | 
					  "status.copy": "Copïo dolen i'r post",
 | 
				
			||||||
  "status.delete": "Dileu",
 | 
					  "status.delete": "Dileu",
 | 
				
			||||||
  "status.detailed_status": "Golwg manwl o'r sgwrs",
 | 
					  "status.detailed_status": "Golwg manwl o'r sgwrs",
 | 
				
			||||||
| 
						 | 
					@ -813,6 +814,7 @@
 | 
				
			||||||
  "status.reblogs.empty": "Does neb wedi hybio'r post yma eto. Pan y bydd rhywun yn gwneud, byddent yn ymddangos yma.",
 | 
					  "status.reblogs.empty": "Does neb wedi hybio'r post yma eto. Pan y bydd rhywun yn gwneud, byddent yn ymddangos yma.",
 | 
				
			||||||
  "status.redraft": "Dileu ac ailddrafftio",
 | 
					  "status.redraft": "Dileu ac ailddrafftio",
 | 
				
			||||||
  "status.remove_bookmark": "Tynnu nod tudalen",
 | 
					  "status.remove_bookmark": "Tynnu nod tudalen",
 | 
				
			||||||
 | 
					  "status.replied_in_thread": "Atebodd mewn edefyn",
 | 
				
			||||||
  "status.replied_to": "Wedi ateb {name}",
 | 
					  "status.replied_to": "Wedi ateb {name}",
 | 
				
			||||||
  "status.reply": "Ateb",
 | 
					  "status.reply": "Ateb",
 | 
				
			||||||
  "status.replyAll": "Ateb i edefyn",
 | 
					  "status.replyAll": "Ateb i edefyn",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -457,7 +457,7 @@
 | 
				
			||||||
  "lists.subheading": "Dine lister",
 | 
					  "lists.subheading": "Dine lister",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# nyt emne} other {# nye emner}}",
 | 
					  "load_pending": "{count, plural, one {# nyt emne} other {# nye emner}}",
 | 
				
			||||||
  "loading_indicator.label": "Indlæser…",
 | 
					  "loading_indicator.label": "Indlæser…",
 | 
				
			||||||
  "media_gallery.toggle_visible": "{number, plural, one {Skjul billede} other {Skjul billeder}}",
 | 
					  "media_gallery.hide": "Skjul",
 | 
				
			||||||
  "moved_to_account_banner.text": "Din konto {disabledAccount} er pt. deaktiveret, da du flyttede til {movedToAccount}.",
 | 
					  "moved_to_account_banner.text": "Din konto {disabledAccount} er pt. deaktiveret, da du flyttede til {movedToAccount}.",
 | 
				
			||||||
  "mute_modal.hide_from_notifications": "Skjul fra notifikationer",
 | 
					  "mute_modal.hide_from_notifications": "Skjul fra notifikationer",
 | 
				
			||||||
  "mute_modal.hide_options": "Skjul valgmuligheder",
 | 
					  "mute_modal.hide_options": "Skjul valgmuligheder",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -457,7 +457,7 @@
 | 
				
			||||||
  "lists.subheading": "Deine Listen",
 | 
					  "lists.subheading": "Deine Listen",
 | 
				
			||||||
  "load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}",
 | 
					  "load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}",
 | 
				
			||||||
  "loading_indicator.label": "Wird geladen …",
 | 
					  "loading_indicator.label": "Wird geladen …",
 | 
				
			||||||
  "media_gallery.toggle_visible": "{number, plural, one {Medium ausblenden} other {Medien ausblenden}}",
 | 
					  "media_gallery.hide": "Ausblenden",
 | 
				
			||||||
  "moved_to_account_banner.text": "Dein Konto {disabledAccount} ist derzeit deaktiviert, weil du zu {movedToAccount} umgezogen bist.",
 | 
					  "moved_to_account_banner.text": "Dein Konto {disabledAccount} ist derzeit deaktiviert, weil du zu {movedToAccount} umgezogen bist.",
 | 
				
			||||||
  "mute_modal.hide_from_notifications": "Benachrichtigungen ausblenden",
 | 
					  "mute_modal.hide_from_notifications": "Benachrichtigungen ausblenden",
 | 
				
			||||||
  "mute_modal.hide_options": "Einstellungen ausblenden",
 | 
					  "mute_modal.hide_options": "Einstellungen ausblenden",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue