Merge pull request #1209 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
		
						commit
						b72317c2b7
					
				
					 82 changed files with 1670 additions and 346 deletions
				
			
		
							
								
								
									
										2
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Gemfile
									
									
									
									
									
								
							| 
						 | 
					@ -94,7 +94,7 @@ gem 'tzinfo-data', '~> 1.2019'
 | 
				
			||||||
gem 'webpacker', '~> 4.0'
 | 
					gem 'webpacker', '~> 4.0'
 | 
				
			||||||
gem 'webpush'
 | 
					gem 'webpush'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gem 'json-ld', git: 'https://github.com/ruby-rdf/json-ld.git', ref: '345b7a5733308af827e8491d284dbafa9128d7a2'
 | 
					gem 'json-ld', git: 'https://github.com/ruby-rdf/json-ld.git', ref: 'e742697a0906e74e8bb777ef98137bc3955d981d'
 | 
				
			||||||
gem 'json-ld-preloaded', '~> 3.0'
 | 
					gem 'json-ld-preloaded', '~> 3.0'
 | 
				
			||||||
gem 'rdf-normalize', '~> 0.3'
 | 
					gem 'rdf-normalize', '~> 0.3'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,8 +7,8 @@ GIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
GIT
 | 
					GIT
 | 
				
			||||||
  remote: https://github.com/ruby-rdf/json-ld.git
 | 
					  remote: https://github.com/ruby-rdf/json-ld.git
 | 
				
			||||||
  revision: 345b7a5733308af827e8491d284dbafa9128d7a2
 | 
					  revision: e742697a0906e74e8bb777ef98137bc3955d981d
 | 
				
			||||||
  ref: 345b7a5733308af827e8491d284dbafa9128d7a2
 | 
					  ref: e742697a0906e74e8bb777ef98137bc3955d981d
 | 
				
			||||||
  specs:
 | 
					  specs:
 | 
				
			||||||
    json-ld (3.0.2)
 | 
					    json-ld (3.0.2)
 | 
				
			||||||
      htmlentities (~> 4.3)
 | 
					      htmlentities (~> 4.3)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,14 @@ class Api::BaseController < ApplicationController
 | 
				
			||||||
    render json: { error: 'This action is not allowed' }, status: 403
 | 
					    render json: { error: 'This action is not allowed' }, status: 403
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  rescue_from Mastodon::RaceConditionError do
 | 
				
			||||||
 | 
					    render json: { error: 'There was a temporary problem serving your request, please try again' }, status: 503
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  rescue_from ActionController::ParameterMissing do |e|
 | 
				
			||||||
 | 
					    render json: { error: e.to_s }, status: 400
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def doorkeeper_unauthorized_render_options(error: nil)
 | 
					  def doorkeeper_unauthorized_render_options(error: nil)
 | 
				
			||||||
    { json: { error: (error.try(:description) || 'Not authorized') } }
 | 
					    { json: { error: (error.try(:description) || 'Not authorized') } }
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,14 +29,13 @@ class Api::V1::Accounts::StatusesController < Api::BaseController
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def account_statuses
 | 
					  def account_statuses
 | 
				
			||||||
    statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
 | 
					    statuses = truthy_param?(:pinned) ? pinned_scope : permitted_account_statuses
 | 
				
			||||||
    statuses = statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    statuses.merge!(only_media_scope) if truthy_param?(:only_media)
 | 
					    statuses.merge!(only_media_scope) if truthy_param?(:only_media)
 | 
				
			||||||
    statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
 | 
					    statuses.merge!(no_replies_scope) if truthy_param?(:exclude_replies)
 | 
				
			||||||
    statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
 | 
					    statuses.merge!(no_reblogs_scope) if truthy_param?(:exclude_reblogs)
 | 
				
			||||||
    statuses.merge!(hashtag_scope)    if params[:tagged].present?
 | 
					    statuses.merge!(hashtag_scope)    if params[:tagged].present?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    statuses
 | 
					    statuses.paginate_by_id(limit_param(DEFAULT_STATUSES_LIMIT), params_slice(:max_id, :since_id, :min_id))
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def permitted_account_statuses
 | 
					  def permitted_account_statuses
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										30
									
								
								app/controllers/api/v1/directories_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								app/controllers/api/v1/directories_controller.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,30 @@
 | 
				
			||||||
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Api::V1::DirectoriesController < Api::BaseController
 | 
				
			||||||
 | 
					  before_action :require_enabled!
 | 
				
			||||||
 | 
					  before_action :set_accounts
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def show
 | 
				
			||||||
 | 
					    render json: @accounts, each_serializer: REST::AccountSerializer
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def require_enabled!
 | 
				
			||||||
 | 
					    return not_found unless Setting.profile_directory
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def set_accounts
 | 
				
			||||||
 | 
					    @accounts = accounts_scope.offset(params[:offset]).limit(limit_param(DEFAULT_ACCOUNTS_LIMIT))
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def accounts_scope
 | 
				
			||||||
 | 
					    Account.discoverable.tap do |scope|
 | 
				
			||||||
 | 
					      scope.merge!(Account.local)                                          if truthy_param?(:local)
 | 
				
			||||||
 | 
					      scope.merge!(Account.by_recent_status)                               if params[:order].blank? || params[:order] == 'active'
 | 
				
			||||||
 | 
					      scope.merge!(Account.order(id: :desc))                               if params[:order] == 'new'
 | 
				
			||||||
 | 
					      scope.merge!(Account.not_excluded_by_account(current_account))       if current_account
 | 
				
			||||||
 | 
					      scope.merge!(Account.not_domain_blocked_by_account(current_account)) if current_account && !truthy_param?(:local)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -22,11 +22,13 @@ class ApplicationController < ActionController::Base
 | 
				
			||||||
  helper_method :whitelist_mode?
 | 
					  helper_method :whitelist_mode?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  rescue_from ActionController::RoutingError, with: :not_found
 | 
					  rescue_from ActionController::RoutingError, with: :not_found
 | 
				
			||||||
  rescue_from ActiveRecord::RecordNotFound, with: :not_found
 | 
					 | 
				
			||||||
  rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
 | 
					  rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
 | 
				
			||||||
  rescue_from ActionController::UnknownFormat, with: :not_acceptable
 | 
					  rescue_from ActionController::UnknownFormat, with: :not_acceptable
 | 
				
			||||||
 | 
					  rescue_from ActionController::ParameterMissing, with: :bad_request
 | 
				
			||||||
 | 
					  rescue_from ActiveRecord::RecordNotFound, with: :not_found
 | 
				
			||||||
  rescue_from Mastodon::NotPermittedError, with: :forbidden
 | 
					  rescue_from Mastodon::NotPermittedError, with: :forbidden
 | 
				
			||||||
  rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
 | 
					  rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
 | 
				
			||||||
 | 
					  rescue_from Mastodon::RaceConditionError, with: :service_unavailable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
 | 
					  before_action :store_current_location, except: :raise_not_found, unless: :devise_controller?
 | 
				
			||||||
  before_action :require_functional!, if: :user_signed_in?
 | 
					  before_action :require_functional!, if: :user_signed_in?
 | 
				
			||||||
| 
						 | 
					@ -166,10 +168,18 @@ class ApplicationController < ActionController::Base
 | 
				
			||||||
    respond_with_error(406)
 | 
					    respond_with_error(406)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def bad_request
 | 
				
			||||||
 | 
					    respond_with_error(400)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def internal_server_error
 | 
					  def internal_server_error
 | 
				
			||||||
    respond_with_error(500)
 | 
					    respond_with_error(500)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def service_unavailable
 | 
				
			||||||
 | 
					    respond_with_error(503)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def single_user_mode?
 | 
					  def single_user_mode?
 | 
				
			||||||
    @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
 | 
					    @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,6 @@ class DirectoriesController < ApplicationController
 | 
				
			||||||
  before_action :require_enabled!
 | 
					  before_action :require_enabled!
 | 
				
			||||||
  before_action :set_instance_presenter
 | 
					  before_action :set_instance_presenter
 | 
				
			||||||
  before_action :set_tag, only: :show
 | 
					  before_action :set_tag, only: :show
 | 
				
			||||||
  before_action :set_tags
 | 
					 | 
				
			||||||
  before_action :set_accounts
 | 
					  before_action :set_accounts
 | 
				
			||||||
  before_action :set_pack
 | 
					  before_action :set_pack
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,13 +32,10 @@ class DirectoriesController < ApplicationController
 | 
				
			||||||
    @tag = Tag.discoverable.find_normalized!(params[:id])
 | 
					    @tag = Tag.discoverable.find_normalized!(params[:id])
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_tags
 | 
					 | 
				
			||||||
    @tags = Tag.discoverable.limit(30).reject { |tag| tag.cached_sample_accounts.empty? }
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def set_accounts
 | 
					  def set_accounts
 | 
				
			||||||
    @accounts = Account.discoverable.by_recent_status.page(params[:page]).per(40).tap do |query|
 | 
					    @accounts = Account.local.discoverable.by_recent_status.page(params[:page]).per(20).tap do |query|
 | 
				
			||||||
      query.merge!(Account.tagged_with(@tag.id)) if @tag
 | 
					      query.merge!(Account.tagged_with(@tag.id)) if @tag
 | 
				
			||||||
 | 
					      query.merge!(Account.not_excluded_by_account(current_account)) if current_account
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,7 +30,7 @@ class RemoteFollowController < ApplicationController
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def session_params
 | 
					  def session_params
 | 
				
			||||||
    { acct: session[:remote_follow] }
 | 
					    { acct: session[:remote_follow] || current_account&.username }
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_pack
 | 
					  def set_pack
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,7 @@ class RemoteInteractionController < ApplicationController
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def session_params
 | 
					  def session_params
 | 
				
			||||||
    { acct: session[:remote_follow] }
 | 
					    { acct: session[:remote_follow] || current_account&.username }
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_status
 | 
					  def set_status
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@ module WellKnown
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expires_in 3.days, public: true
 | 
					      expires_in 3.days, public: true
 | 
				
			||||||
      render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json'
 | 
					      render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json'
 | 
				
			||||||
    rescue ActiveRecord::RecordNotFound
 | 
					    rescue ActiveRecord::RecordNotFound, ActionController::ParameterMissing
 | 
				
			||||||
      head 404
 | 
					      head 404
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,6 +34,26 @@ module StatusesHelper
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def minimal_account_action_button(account)
 | 
				
			||||||
 | 
					    if user_signed_in?
 | 
				
			||||||
 | 
					      return if account.id == current_user.account_id
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if current_account.following?(account) || current_account.requested?(account)
 | 
				
			||||||
 | 
					        link_to account_unfollow_path(account), class: 'icon-button active', data: { method: :post }, title: t('accounts.unfollow') do
 | 
				
			||||||
 | 
					          fa_icon('user-times fw')
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      elsif !(account.memorial? || account.moved?)
 | 
				
			||||||
 | 
					        link_to account_follow_path(account), class: "icon-button#{account.blocking?(current_account) ? ' disabled' : ''}", data: { method: :post }, title: t('accounts.follow') do
 | 
				
			||||||
 | 
					          fa_icon('user-plus fw')
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    elsif !(account.memorial? || account.moved?)
 | 
				
			||||||
 | 
					      link_to account_remote_follow_path(account), class: 'icon-button modal-button', target: '_new', title: t('accounts.follow') do
 | 
				
			||||||
 | 
					        fa_icon('user-plus fw')
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def svg_logo
 | 
					  def svg_logo
 | 
				
			||||||
    content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
 | 
					    content_tag(:svg, tag(:use, 'xlink:href' => '#mastodon-svg-logo'), 'viewBox' => '0 0 216.4144 232.00976')
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										61
									
								
								app/javascript/flavours/glitch/actions/directory.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								app/javascript/flavours/glitch/actions/directory.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,61 @@
 | 
				
			||||||
 | 
					import api from 'flavours/glitch/util/api';
 | 
				
			||||||
 | 
					import { importFetchedAccounts } from './importer';
 | 
				
			||||||
 | 
					import { fetchRelationships } from './accounts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
 | 
				
			||||||
 | 
					export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
 | 
				
			||||||
 | 
					export const DIRECTORY_FETCH_FAIL    = 'DIRECTORY_FETCH_FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
 | 
				
			||||||
 | 
					export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
 | 
				
			||||||
 | 
					export const DIRECTORY_EXPAND_FAIL    = 'DIRECTORY_EXPAND_FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fetchDirectory = params => (dispatch, getState) => {
 | 
				
			||||||
 | 
					  dispatch(fetchDirectoryRequest());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
 | 
				
			||||||
 | 
					    dispatch(importFetchedAccounts(data));
 | 
				
			||||||
 | 
					    dispatch(fetchDirectorySuccess(data));
 | 
				
			||||||
 | 
					    dispatch(fetchRelationships(data.map(x => x.id)));
 | 
				
			||||||
 | 
					  }).catch(error => dispatch(fetchDirectoryFail(error)));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fetchDirectoryRequest = () => ({
 | 
				
			||||||
 | 
					  type: DIRECTORY_FETCH_REQUEST,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fetchDirectorySuccess = accounts => ({
 | 
				
			||||||
 | 
					  type: DIRECTORY_FETCH_SUCCESS,
 | 
				
			||||||
 | 
					  accounts,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fetchDirectoryFail = error => ({
 | 
				
			||||||
 | 
					  type: DIRECTORY_FETCH_FAIL,
 | 
				
			||||||
 | 
					  error,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const expandDirectory = params => (dispatch, getState) => {
 | 
				
			||||||
 | 
					  dispatch(expandDirectoryRequest());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
 | 
				
			||||||
 | 
					    dispatch(importFetchedAccounts(data));
 | 
				
			||||||
 | 
					    dispatch(expandDirectorySuccess(data));
 | 
				
			||||||
 | 
					    dispatch(fetchRelationships(data.map(x => x.id)));
 | 
				
			||||||
 | 
					  }).catch(error => dispatch(expandDirectoryFail(error)));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const expandDirectoryRequest = () => ({
 | 
				
			||||||
 | 
					  type: DIRECTORY_EXPAND_REQUEST,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const expandDirectorySuccess = accounts => ({
 | 
				
			||||||
 | 
					  type: DIRECTORY_EXPAND_SUCCESS,
 | 
				
			||||||
 | 
					  accounts,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const expandDirectoryFail = error => ({
 | 
				
			||||||
 | 
					  type: DIRECTORY_EXPAND_FAIL,
 | 
				
			||||||
 | 
					  error,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import api from '../api';
 | 
					import api from 'flavours/glitch/util/api';
 | 
				
			||||||
import { importFetchedPoll } from './importer';
 | 
					import { importFetchedPoll } from './importer';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';
 | 
					export const POLL_VOTE_REQUEST = 'POLL_VOTE_REQUEST';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,11 +4,11 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
				
			||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
import classNames from 'classnames';
 | 
					import classNames from 'classnames';
 | 
				
			||||||
import { vote, fetchPoll } from 'mastodon/actions/polls';
 | 
					import { vote, fetchPoll } from 'flavours/glitch/actions/polls';
 | 
				
			||||||
import Motion from 'mastodon/features/ui/util/optional_motion';
 | 
					import Motion from 'flavours/glitch/util/optional_motion';
 | 
				
			||||||
import spring from 'react-motion/lib/spring';
 | 
					import spring from 'react-motion/lib/spring';
 | 
				
			||||||
import escapeTextContentForBrowser from 'escape-html';
 | 
					import escapeTextContentForBrowser from 'escape-html';
 | 
				
			||||||
import emojify from 'mastodon/features/emoji/emoji';
 | 
					import emojify from 'flavours/glitch/util/emoji';
 | 
				
			||||||
import RelativeTimestamp from './relative_timestamp';
 | 
					import RelativeTimestamp from './relative_timestamp';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										35
									
								
								app/javascript/flavours/glitch/components/radio_button.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/javascript/flavours/glitch/components/radio_button.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class RadioButton extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static propTypes = {
 | 
				
			||||||
 | 
					    value: PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    checked: PropTypes.bool,
 | 
				
			||||||
 | 
					    name: PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    onChange: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    label: PropTypes.node.isRequired,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { name, value, checked, onChange, label } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <label className='radio-button'>
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          name={name}
 | 
				
			||||||
 | 
					          type='radio'
 | 
				
			||||||
 | 
					          value={value}
 | 
				
			||||||
 | 
					          checked={checked}
 | 
				
			||||||
 | 
					          onChange={onChange}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <span className={classNames('radio-button__input', { checked })} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <span>{label}</span>
 | 
				
			||||||
 | 
					      </label>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 | 
				
			||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
				
			||||||
import { autoPlayGif, me, isStaff } from 'flavours/glitch/util/initial_state';
 | 
					import { autoPlayGif, me, isStaff } from 'flavours/glitch/util/initial_state';
 | 
				
			||||||
 | 
					import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/util/backend_links';
 | 
				
			||||||
import classNames from 'classnames';
 | 
					import classNames from 'classnames';
 | 
				
			||||||
import Icon from 'flavours/glitch/components/icon';
 | 
					import Icon from 'flavours/glitch/components/icon';
 | 
				
			||||||
import Avatar from 'flavours/glitch/components/avatar';
 | 
					import Avatar from 'flavours/glitch/components/avatar';
 | 
				
			||||||
| 
						 | 
					@ -69,7 +70,7 @@ class Header extends ImmutablePureComponent {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  openEditProfile = () => {
 | 
					  openEditProfile = () => {
 | 
				
			||||||
    window.open('/settings/profile', '_blank');
 | 
					    window.open(profileLink, '_blank');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  _updateEmojis () {
 | 
					  _updateEmojis () {
 | 
				
			||||||
| 
						 | 
					@ -148,7 +149,7 @@ class Header extends ImmutablePureComponent {
 | 
				
			||||||
      } else if (account.getIn(['relationship', 'blocking'])) {
 | 
					      } else if (account.getIn(['relationship', 'blocking'])) {
 | 
				
			||||||
        actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
 | 
					        actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else if (profileLink) {
 | 
				
			||||||
      actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
 | 
					      actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -172,8 +173,8 @@ class Header extends ImmutablePureComponent {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (account.get('id') === me) {
 | 
					    if (account.get('id') === me) {
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
 | 
					      if (profileLink) menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink });
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
 | 
					      if (preferencesLink) menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink });
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
 | 
					      menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
 | 
				
			||||||
      menu.push(null);
 | 
					      menu.push(null);
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
 | 
					      menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
 | 
				
			||||||
| 
						 | 
					@ -223,9 +224,9 @@ class Header extends ImmutablePureComponent {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (account.get('id') !== me && isStaff) {
 | 
					    if (account.get('id') !== me && isStaff && accountAdminLink) {
 | 
				
			||||||
      menu.push(null);
 | 
					      menu.push(null);
 | 
				
			||||||
      menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
 | 
					      menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: accountAdminLink(account.get('id')) });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const content          = { __html: account.get('note_emojified') };
 | 
					    const content          = { __html: account.get('note_emojified') };
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
import { connect } from 'react-redux';
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
import SearchResults from '../components/search_results';
 | 
					import SearchResults from '../components/search_results';
 | 
				
			||||||
import { fetchSuggestions, dismissSuggestion } from 'mastodon/actions/suggestions';
 | 
					import { fetchSuggestions, dismissSuggestion } from 'flavours/glitch/actions/suggestions';
 | 
				
			||||||
import { expandSearch } from 'mastodon/actions/search';
 | 
					import { expandSearch } from 'flavours/glitch/actions/search';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
  results: state.getIn(['search', 'results']),
 | 
					  results: state.getIn(['search', 'results']),
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import Warning from '../components/warning';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
import { me } from 'flavours/glitch/util/initial_state';
 | 
					import { me } from 'flavours/glitch/util/initial_state';
 | 
				
			||||||
 | 
					import { profileLink, termsLink } from 'flavours/glitch/util/backend_links';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;
 | 
					const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +16,7 @@ const mapStateToProps = state => ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => {
 | 
					const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => {
 | 
				
			||||||
  if (needsLockWarning) {
 | 
					  if (needsLockWarning) {
 | 
				
			||||||
    return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
 | 
					    return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href={profileLink}><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (hashtagWarning) {
 | 
					  if (hashtagWarning) {
 | 
				
			||||||
| 
						 | 
					@ -25,7 +26,7 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning
 | 
				
			||||||
  if (directMessageWarning) {
 | 
					  if (directMessageWarning) {
 | 
				
			||||||
    const message = (
 | 
					    const message = (
 | 
				
			||||||
      <span>
 | 
					      <span>
 | 
				
			||||||
        <FormattedMessage id='compose_form.direct_message_warning' defaultMessage='This toot will only be sent to all the mentioned users.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>
 | 
					        <FormattedMessage id='compose_form.direct_message_warning' defaultMessage='This toot will only be sent to all the mentioned users.' /> {!!termsLink && <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>}
 | 
				
			||||||
      </span>
 | 
					      </span>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,149 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
				
			||||||
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import { makeGetAccount } from 'flavours/glitch/selectors';
 | 
				
			||||||
 | 
					import Avatar from 'flavours/glitch/components/avatar';
 | 
				
			||||||
 | 
					import DisplayName from 'flavours/glitch/components/display_name';
 | 
				
			||||||
 | 
					import Permalink from 'flavours/glitch/components/permalink';
 | 
				
			||||||
 | 
					import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp';
 | 
				
			||||||
 | 
					import IconButton from 'flavours/glitch/components/icon_button';
 | 
				
			||||||
 | 
					import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
 | 
				
			||||||
 | 
					import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/util/initial_state';
 | 
				
			||||||
 | 
					import { shortNumberFormat } from 'flavours/glitch/util/numbers';
 | 
				
			||||||
 | 
					import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'flavours/glitch/actions/accounts';
 | 
				
			||||||
 | 
					import { openModal } from 'flavours/glitch/actions/modal';
 | 
				
			||||||
 | 
					import { initMuteModal } from 'flavours/glitch/actions/mutes';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  follow: { id: 'account.follow', defaultMessage: 'Follow' },
 | 
				
			||||||
 | 
					  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
 | 
				
			||||||
 | 
					  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
 | 
				
			||||||
 | 
					  unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
 | 
				
			||||||
 | 
					  unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const makeMapStateToProps = () => {
 | 
				
			||||||
 | 
					  const getAccount = makeGetAccount();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mapStateToProps = (state, { id }) => ({
 | 
				
			||||||
 | 
					    account: getAccount(state, id),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return mapStateToProps;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapDispatchToProps = (dispatch, { intl }) => ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onFollow (account) {
 | 
				
			||||||
 | 
					    if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
 | 
				
			||||||
 | 
					      if (unfollowModal) {
 | 
				
			||||||
 | 
					        dispatch(openModal('CONFIRM', {
 | 
				
			||||||
 | 
					          message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
 | 
				
			||||||
 | 
					          confirm: intl.formatMessage(messages.unfollowConfirm),
 | 
				
			||||||
 | 
					          onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
 | 
				
			||||||
 | 
					        }));
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        dispatch(unfollowAccount(account.get('id')));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      dispatch(followAccount(account.get('id')));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onBlock (account) {
 | 
				
			||||||
 | 
					    if (account.getIn(['relationship', 'blocking'])) {
 | 
				
			||||||
 | 
					      dispatch(unblockAccount(account.get('id')));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      dispatch(blockAccount(account.get('id')));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onMute (account) {
 | 
				
			||||||
 | 
					    if (account.getIn(['relationship', 'muting'])) {
 | 
				
			||||||
 | 
					      dispatch(unmuteAccount(account.get('id')));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      dispatch(initMuteModal(account));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default @injectIntl
 | 
				
			||||||
 | 
					@connect(makeMapStateToProps, mapDispatchToProps)
 | 
				
			||||||
 | 
					class AccountCard extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static propTypes = {
 | 
				
			||||||
 | 
					    account: ImmutablePropTypes.map.isRequired,
 | 
				
			||||||
 | 
					    intl: PropTypes.object.isRequired,
 | 
				
			||||||
 | 
					    onFollow: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    onBlock: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    onMute: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleFollow = () => {
 | 
				
			||||||
 | 
					    this.props.onFollow(this.props.account);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleBlock = () => {
 | 
				
			||||||
 | 
					    this.props.onBlock(this.props.account);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleMute = () => {
 | 
				
			||||||
 | 
					    this.props.onMute(this.props.account);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { account, intl } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let buttons;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (account.get('id') !== me && account.get('relationship', null) !== null) {
 | 
				
			||||||
 | 
					      const following = account.getIn(['relationship', 'following']);
 | 
				
			||||||
 | 
					      const requested = account.getIn(['relationship', 'requested']);
 | 
				
			||||||
 | 
					      const blocking  = account.getIn(['relationship', 'blocking']);
 | 
				
			||||||
 | 
					      const muting    = account.getIn(['relationship', 'muting']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (requested) {
 | 
				
			||||||
 | 
					        buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
 | 
				
			||||||
 | 
					      } else if (blocking) {
 | 
				
			||||||
 | 
					        buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
 | 
				
			||||||
 | 
					      } else if (muting) {
 | 
				
			||||||
 | 
					        buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
 | 
				
			||||||
 | 
					      } else if (!account.get('moved') || following) {
 | 
				
			||||||
 | 
					        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className='directory__card'>
 | 
				
			||||||
 | 
					        <div className='directory__card__img'>
 | 
				
			||||||
 | 
					          <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className='directory__card__bar'>
 | 
				
			||||||
 | 
					          <Permalink className='directory__card__bar__name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
 | 
				
			||||||
 | 
					            <Avatar account={account} size={48} />
 | 
				
			||||||
 | 
					            <DisplayName account={account} />
 | 
				
			||||||
 | 
					          </Permalink>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className='directory__card__bar__relationship account__relationship'>
 | 
				
			||||||
 | 
					            {buttons}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className='directory__card__extra'>
 | 
				
			||||||
 | 
					          <div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className='directory__card__extra'>
 | 
				
			||||||
 | 
					          <div className='accounts-table__count'>{shortNumberFormat(account.get('statuses_count'))} <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div>
 | 
				
			||||||
 | 
					          <div className='accounts-table__count'>{account.get('followers_count') < 0 ? '-' : shortNumberFormat(account.get('followers_count'))} <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div>
 | 
				
			||||||
 | 
					          <div className='accounts-table__count'>{account.get('last_status_at') === null ? <FormattedMessage id='account.never_active' defaultMessage='Never' /> : <RelativeTimestamp timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										171
									
								
								app/javascript/flavours/glitch/features/directory/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								app/javascript/flavours/glitch/features/directory/index.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,171 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import { defineMessages, injectIntl } from 'react-intl';
 | 
				
			||||||
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
 | 
					import Column from 'flavours/glitch/components/column';
 | 
				
			||||||
 | 
					import ColumnHeader from 'flavours/glitch/components/column_header';
 | 
				
			||||||
 | 
					import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'flavours/glitch/actions/columns';
 | 
				
			||||||
 | 
					import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directory';
 | 
				
			||||||
 | 
					import { List as ImmutableList } from 'immutable';
 | 
				
			||||||
 | 
					import AccountCard from './components/account_card';
 | 
				
			||||||
 | 
					import RadioButton from 'flavours/glitch/components/radio_button';
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					import LoadMore from 'flavours/glitch/components/load_more';
 | 
				
			||||||
 | 
					import { ScrollContainer } from 'react-router-scroll-4';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
 | 
				
			||||||
 | 
					  recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' },
 | 
				
			||||||
 | 
					  newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
 | 
				
			||||||
 | 
					  local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
 | 
				
			||||||
 | 
					  federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
 | 
					  accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()),
 | 
				
			||||||
 | 
					  isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true),
 | 
				
			||||||
 | 
					  domain: state.getIn(['meta', 'domain']),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default @connect(mapStateToProps)
 | 
				
			||||||
 | 
					@injectIntl
 | 
				
			||||||
 | 
					class Directory extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static contextTypes = {
 | 
				
			||||||
 | 
					    router: PropTypes.object,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static propTypes = {
 | 
				
			||||||
 | 
					    isLoading: PropTypes.bool,
 | 
				
			||||||
 | 
					    accountIds: ImmutablePropTypes.list.isRequired,
 | 
				
			||||||
 | 
					    dispatch: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    shouldUpdateScroll: PropTypes.func,
 | 
				
			||||||
 | 
					    columnId: PropTypes.string,
 | 
				
			||||||
 | 
					    intl: PropTypes.object.isRequired,
 | 
				
			||||||
 | 
					    multiColumn: PropTypes.bool,
 | 
				
			||||||
 | 
					    domain: PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    params: PropTypes.shape({
 | 
				
			||||||
 | 
					      order: PropTypes.string,
 | 
				
			||||||
 | 
					      local: PropTypes.bool,
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  state = {
 | 
				
			||||||
 | 
					    order: null,
 | 
				
			||||||
 | 
					    local: null,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handlePin = () => {
 | 
				
			||||||
 | 
					    const { columnId, dispatch } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (columnId) {
 | 
				
			||||||
 | 
					      dispatch(removeColumn(columnId));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      dispatch(addColumn('DIRECTORY', this.getParams(this.props, this.state)));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getParams = (props, state) => ({
 | 
				
			||||||
 | 
					    order: state.order === null ? (props.params.order || 'active') : state.order,
 | 
				
			||||||
 | 
					    local: state.local === null ? (props.params.local || false) : state.local,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleMove = dir => {
 | 
				
			||||||
 | 
					    const { columnId, dispatch } = this.props;
 | 
				
			||||||
 | 
					    dispatch(moveColumn(columnId, dir));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleHeaderClick = () => {
 | 
				
			||||||
 | 
					    this.column.scrollTop();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  componentDidMount () {
 | 
				
			||||||
 | 
					    const { dispatch } = this.props;
 | 
				
			||||||
 | 
					    dispatch(fetchDirectory(this.getParams(this.props, this.state)));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  componentDidUpdate (prevProps, prevState) {
 | 
				
			||||||
 | 
					    const { dispatch } = this.props;
 | 
				
			||||||
 | 
					    const paramsOld = this.getParams(prevProps, prevState);
 | 
				
			||||||
 | 
					    const paramsNew = this.getParams(this.props, this.state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) {
 | 
				
			||||||
 | 
					      dispatch(fetchDirectory(paramsNew));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setRef = c => {
 | 
				
			||||||
 | 
					    this.column = c;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleChangeOrder = e => {
 | 
				
			||||||
 | 
					    const { dispatch, columnId } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (columnId) {
 | 
				
			||||||
 | 
					      dispatch(changeColumnParams(columnId, ['order'], e.target.value));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      this.setState({ order: e.target.value });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleChangeLocal = e => {
 | 
				
			||||||
 | 
					    const { dispatch, columnId } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (columnId) {
 | 
				
			||||||
 | 
					      dispatch(changeColumnParams(columnId, ['local'], e.target.value === '1'));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      this.setState({ local: e.target.value === '1' });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleLoadMore = () => {
 | 
				
			||||||
 | 
					    const { dispatch } = this.props;
 | 
				
			||||||
 | 
					    dispatch(expandDirectory(this.getParams(this.props, this.state)));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { isLoading, accountIds, intl, columnId, multiColumn, domain, shouldUpdateScroll } = this.props;
 | 
				
			||||||
 | 
					    const { order, local }  = this.getParams(this.props, this.state);
 | 
				
			||||||
 | 
					    const pinned = !!columnId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const scrollableArea = (
 | 
				
			||||||
 | 
					      <div className='scrollable' style={{ background: 'transparent' }}>
 | 
				
			||||||
 | 
					        <div className='filter-form'>
 | 
				
			||||||
 | 
					          <div className='filter-form__column' role='group'>
 | 
				
			||||||
 | 
					            <RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
 | 
				
			||||||
 | 
					            <RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={this.handleChangeOrder} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className='filter-form__column' role='group'>
 | 
				
			||||||
 | 
					            <RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain })} checked={local} onChange={this.handleChangeLocal} />
 | 
				
			||||||
 | 
					            <RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={this.handleChangeLocal} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className={classNames('directory__list', { loading: isLoading })}>
 | 
				
			||||||
 | 
					          {accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
 | 
				
			||||||
 | 
					        <ColumnHeader
 | 
				
			||||||
 | 
					          icon='address-book-o'
 | 
				
			||||||
 | 
					          title={intl.formatMessage(messages.title)}
 | 
				
			||||||
 | 
					          onPin={this.handlePin}
 | 
				
			||||||
 | 
					          onMove={this.handleMove}
 | 
				
			||||||
 | 
					          onClick={this.handleHeaderClick}
 | 
				
			||||||
 | 
					          pinned={pinned}
 | 
				
			||||||
 | 
					          multiColumn={multiColumn}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {multiColumn && !pinned ? <ScrollContainer scrollKey='directory' shouldUpdateScroll={shouldUpdateScroll}>{scrollableArea}</ScrollContainer> : scrollableArea}
 | 
				
			||||||
 | 
					      </Column>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ import { openModal } from 'flavours/glitch/actions/modal';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
				
			||||||
import { me } from 'flavours/glitch/util/initial_state';
 | 
					import { me, profile_directory } from 'flavours/glitch/util/initial_state';
 | 
				
			||||||
import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
 | 
					import { fetchFollowRequests } from 'flavours/glitch/actions/accounts';
 | 
				
			||||||
import { List as ImmutableList } from 'immutable';
 | 
					import { List as ImmutableList } from 'immutable';
 | 
				
			||||||
import { createSelector } from 'reselect';
 | 
					import { createSelector } from 'reselect';
 | 
				
			||||||
| 
						 | 
					@ -36,6 +36,7 @@ const messages = defineMessages({
 | 
				
			||||||
  lists_subheading: { id: 'column_subheading.lists', defaultMessage: 'Lists' },
 | 
					  lists_subheading: { id: 'column_subheading.lists', defaultMessage: 'Lists' },
 | 
				
			||||||
  misc: { id: 'navigation_bar.misc', defaultMessage: 'Misc' },
 | 
					  misc: { id: 'navigation_bar.misc', defaultMessage: 'Misc' },
 | 
				
			||||||
  menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
 | 
					  menu: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
 | 
				
			||||||
 | 
					  profile_directory: { id: 'getting_started.directory', defaultMessage: 'Profile directory' },
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const makeMapStateToProps = () => {
 | 
					const makeMapStateToProps = () => {
 | 
				
			||||||
| 
						 | 
					@ -150,13 +151,17 @@ const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
 | 
				
			||||||
      navItems.push(<ColumnLink key='6' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
 | 
					      navItems.push(<ColumnLink key='6' icon='user-plus' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    navItems.push(<ColumnLink key='7' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
 | 
					    if (profile_directory) {
 | 
				
			||||||
 | 
					      navItems.push(<ColumnLink key='7' icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    navItems.push(<ColumnLink key='8' icon='ellipsis-h' text={intl.formatMessage(messages.misc)} to='/getting-started-misc' />);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    listItems = listItems.concat([
 | 
					    listItems = listItems.concat([
 | 
				
			||||||
      <div key='8'>
 | 
					      <div key='9'>
 | 
				
			||||||
        <ColumnLink key='9' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
 | 
					        <ColumnLink key='10' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
 | 
				
			||||||
        {lists.map(list =>
 | 
					        {lists.map(list =>
 | 
				
			||||||
          <ColumnLink key={(9 + Number(list.get('id'))).toString()} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
 | 
					          <ColumnLink key={(11 + Number(list.get('id'))).toString()} to={`/timelines/list/${list.get('id')}`} icon='list-ul' text={list.get('title')} />
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
      </div>,
 | 
					      </div>,
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,7 @@ const mapDispatchToProps = (dispatch, { columnId }) => ({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onLoad (value) {
 | 
					  onLoad (value) {
 | 
				
			||||||
    return api().get('/api/v2/search', { params: { q: value } }).then(response => {
 | 
					    return api().get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
 | 
				
			||||||
      return (response.data.hashtags || []).map((tag) => {
 | 
					      return (response.data.hashtags || []).map((tag) => {
 | 
				
			||||||
        return { value: tag.name, label: `#${tag.name}` };
 | 
					        return { value: tag.name, label: `#${tag.name}` };
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,28 +82,38 @@ const makeMapStateToProps = () => {
 | 
				
			||||||
  const getDescendantsIds = createSelector([
 | 
					  const getDescendantsIds = createSelector([
 | 
				
			||||||
    (_, { id }) => id,
 | 
					    (_, { id }) => id,
 | 
				
			||||||
    state => state.getIn(['contexts', 'replies']),
 | 
					    state => state.getIn(['contexts', 'replies']),
 | 
				
			||||||
  ], (statusId, contextReplies) => {
 | 
					    state => state.get('statuses'),
 | 
				
			||||||
    let descendantsIds = Immutable.List();
 | 
					  ], (statusId, contextReplies, statuses) => {
 | 
				
			||||||
    descendantsIds = descendantsIds.withMutations(mutable => {
 | 
					    let descendantsIds = [];
 | 
				
			||||||
      const ids = [statusId];
 | 
					    const ids = [statusId];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      while (ids.length > 0) {
 | 
					    while (ids.length > 0) {
 | 
				
			||||||
        let id        = ids.shift();
 | 
					      let id        = ids.shift();
 | 
				
			||||||
        const replies = contextReplies.get(id);
 | 
					      const replies = contextReplies.get(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (statusId !== id) {
 | 
					      if (statusId !== id) {
 | 
				
			||||||
          mutable.push(id);
 | 
					        descendantsIds.push(id);
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (replies) {
 | 
					 | 
				
			||||||
          replies.reverse().forEach(reply => {
 | 
					 | 
				
			||||||
            ids.unshift(reply);
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return descendantsIds;
 | 
					      if (replies) {
 | 
				
			||||||
 | 
					        replies.reverse().forEach(reply => {
 | 
				
			||||||
 | 
					          ids.unshift(reply);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account'));
 | 
				
			||||||
 | 
					    if (insertAt !== -1) {
 | 
				
			||||||
 | 
					      descendantsIds.forEach((id, idx) => {
 | 
				
			||||||
 | 
					        if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) {
 | 
				
			||||||
 | 
					          descendantsIds.splice(idx, 1);
 | 
				
			||||||
 | 
					          descendantsIds.splice(insertAt, 0, id);
 | 
				
			||||||
 | 
					          insertAt += 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Immutable.List(descendantsIds);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const mapStateToProps = (state, props) => {
 | 
					  const mapStateToProps = (state, props) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,19 @@ import BundleContainer from '../containers/bundle_container';
 | 
				
			||||||
import ColumnLoading from './column_loading';
 | 
					import ColumnLoading from './column_loading';
 | 
				
			||||||
import DrawerLoading from './drawer_loading';
 | 
					import DrawerLoading from './drawer_loading';
 | 
				
			||||||
import BundleColumnError from './bundle_column_error';
 | 
					import BundleColumnError from './bundle_column_error';
 | 
				
			||||||
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, BookmarkedStatuses, ListTimeline } from 'flavours/glitch/util/async-components';
 | 
					import {
 | 
				
			||||||
 | 
					  Compose,
 | 
				
			||||||
 | 
					  Notifications,
 | 
				
			||||||
 | 
					  HomeTimeline,
 | 
				
			||||||
 | 
					  CommunityTimeline,
 | 
				
			||||||
 | 
					  PublicTimeline,
 | 
				
			||||||
 | 
					  HashtagTimeline,
 | 
				
			||||||
 | 
					  DirectTimeline,
 | 
				
			||||||
 | 
					  FavouritedStatuses,
 | 
				
			||||||
 | 
					  BookmarkedStatuses,
 | 
				
			||||||
 | 
					  ListTimeline,
 | 
				
			||||||
 | 
					  Directory,
 | 
				
			||||||
 | 
					} from 'flavours/glitch/util/async-components';
 | 
				
			||||||
import ComposePanel from './compose_panel';
 | 
					import ComposePanel from './compose_panel';
 | 
				
			||||||
import NavigationPanel from './navigation_panel';
 | 
					import NavigationPanel from './navigation_panel';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,6 +42,7 @@ const componentMap = {
 | 
				
			||||||
  'FAVOURITES': FavouritedStatuses,
 | 
					  'FAVOURITES': FavouritedStatuses,
 | 
				
			||||||
  'BOOKMARKS': BookmarkedStatuses,
 | 
					  'BOOKMARKS': BookmarkedStatuses,
 | 
				
			||||||
  'LIST': ListTimeline,
 | 
					  'LIST': ListTimeline,
 | 
				
			||||||
 | 
					  'DIRECTORY': Directory,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started/);
 | 
					const shouldHideFAB = path => path.match(/^\/statuses\/|^\/search|^\/getting-started/);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
 | 
				
			||||||
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 | 
					import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
 | 
				
			||||||
import { Link } from 'react-router-dom';
 | 
					import { Link } from 'react-router-dom';
 | 
				
			||||||
import { invitesEnabled, version, repository, source_url } from 'flavours/glitch/util/initial_state';
 | 
					import { invitesEnabled, version, repository, source_url } from 'flavours/glitch/util/initial_state';
 | 
				
			||||||
import { signOutLink } from 'flavours/glitch/util/backend_links';
 | 
					import { signOutLink, securityLink } from 'flavours/glitch/util/backend_links';
 | 
				
			||||||
import { logOut } from 'flavours/glitch/util/log_out';
 | 
					import { logOut } from 'flavours/glitch/util/log_out';
 | 
				
			||||||
import { openModal } from 'flavours/glitch/actions/modal';
 | 
					import { openModal } from 'flavours/glitch/actions/modal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,7 +46,7 @@ class LinkFooter extends React.PureComponent {
 | 
				
			||||||
      <div className='getting-started__footer'>
 | 
					      <div className='getting-started__footer'>
 | 
				
			||||||
        <ul>
 | 
					        <ul>
 | 
				
			||||||
          {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
 | 
					          {invitesEnabled && <li><a href='/invites' target='_blank'><FormattedMessage id='getting_started.invite' defaultMessage='Invite people' /></a> · </li>}
 | 
				
			||||||
          <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
 | 
					          {!!securityLink && <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>}
 | 
				
			||||||
          <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
 | 
					          <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
 | 
				
			||||||
          <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
 | 
					          <li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
 | 
				
			||||||
          <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
 | 
					          <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,6 +3,7 @@ import { NavLink, withRouter } from 'react-router-dom';
 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
import Icon from 'flavours/glitch/components/icon';
 | 
					import Icon from 'flavours/glitch/components/icon';
 | 
				
			||||||
import { profile_directory } from 'flavours/glitch/util/initial_state';
 | 
					import { profile_directory } from 'flavours/glitch/util/initial_state';
 | 
				
			||||||
 | 
					import { preferencesLink, relationshipsLink } from 'flavours/glitch/util/backend_links';
 | 
				
			||||||
import NotificationsCounterIcon from './notifications_counter_icon';
 | 
					import NotificationsCounterIcon from './notifications_counter_icon';
 | 
				
			||||||
import FollowRequestsNavLink from './follow_requests_nav_link';
 | 
					import FollowRequestsNavLink from './follow_requests_nav_link';
 | 
				
			||||||
import ListPanel from './list_panel';
 | 
					import ListPanel from './list_panel';
 | 
				
			||||||
| 
						 | 
					@ -16,16 +17,16 @@ const NavigationPanel = ({ onOpenSettings }) => (
 | 
				
			||||||
    <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' icon='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
 | 
					    <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' icon='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
 | 
				
			||||||
    <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' icon='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
 | 
					    <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' icon='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
 | 
				
			||||||
    <NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' icon='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
 | 
					    <NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' icon='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
 | 
				
			||||||
 | 
					    {profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' icon='address-book-o' fixedWidth /><FormattedMessage id='getting_started.profile_directory' defaultMessage='Profile directory' /></NavLink>}
 | 
				
			||||||
    <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' icon='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
 | 
					    <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' icon='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <ListPanel />
 | 
					    <ListPanel />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <hr />
 | 
					    <hr />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <a className='column-link column-link--transparent' href='/settings/preferences' target='_blank'><Icon className='column-link__icon' icon='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>
 | 
					    {!!preferencesLink && <a className='column-link column-link--transparent' href={preferencesLink} target='_blank'><Icon className='column-link__icon' icon='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>}
 | 
				
			||||||
    <a className='column-link column-link--transparent' href='#' onClick={onOpenSettings}><Icon className='column-link__icon' icon='cogs' fixedWidth /><FormattedMessage id='navigation_bar.app_settings' defaultMessage='App settings' /></a>
 | 
					    <a className='column-link column-link--transparent' href='#' onClick={onOpenSettings}><Icon className='column-link__icon' icon='cogs' fixedWidth /><FormattedMessage id='navigation_bar.app_settings' defaultMessage='App settings' /></a>
 | 
				
			||||||
    <a className='column-link column-link--transparent' href='/relationships' target='_blank'><Icon className='column-link__icon' icon='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>
 | 
					    {!!relationshipsLink && <a className='column-link column-link--transparent' href={relationshipsLink} target='_blank'><Icon className='column-link__icon' icon='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>}
 | 
				
			||||||
    {!!profile_directory && <a className='column-link column-link--transparent' href='/explore'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></a>}
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,6 +46,7 @@ import {
 | 
				
			||||||
  Lists,
 | 
					  Lists,
 | 
				
			||||||
  Search,
 | 
					  Search,
 | 
				
			||||||
  GettingStartedMisc,
 | 
					  GettingStartedMisc,
 | 
				
			||||||
 | 
					  Directory,
 | 
				
			||||||
} from 'flavours/glitch/util/async-components';
 | 
					} from 'flavours/glitch/util/async-components';
 | 
				
			||||||
import { HotKeys } from 'react-hotkeys';
 | 
					import { HotKeys } from 'react-hotkeys';
 | 
				
			||||||
import { me } from 'flavours/glitch/util/initial_state';
 | 
					import { me } from 'flavours/glitch/util/initial_state';
 | 
				
			||||||
| 
						 | 
					@ -185,6 +186,7 @@ class SwitchingColumnsArea extends React.PureComponent {
 | 
				
			||||||
          <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
 | 
					          <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <WrappedRoute path='/search' component={Search} content={children} />
 | 
					          <WrappedRoute path='/search' component={Search} content={children} />
 | 
				
			||||||
 | 
					          <WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <WrappedRoute path='/statuses/new' component={Compose} content={children} />
 | 
					          <WrappedRoute path='/statuses/new' component={Compose} content={children} />
 | 
				
			||||||
          <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
 | 
					          <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
import { POLLS_IMPORT } from 'mastodon/actions/importer';
 | 
					import { POLLS_IMPORT } from 'flavours/glitch/actions/importer';
 | 
				
			||||||
import { Map as ImmutableMap, fromJS } from 'immutable';
 | 
					import { Map as ImmutableMap, fromJS } from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const importPolls = (state, polls) => state.withMutations(map => polls.forEach(poll => map.set(poll.id, fromJS(poll))));
 | 
					const importPolls = (state, polls) => state.withMutations(map => polls.forEach(poll => map.set(poll.id, fromJS(poll))));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,8 +4,8 @@ import {
 | 
				
			||||||
  SUGGESTIONS_FETCH_FAIL,
 | 
					  SUGGESTIONS_FETCH_FAIL,
 | 
				
			||||||
  SUGGESTIONS_DISMISS,
 | 
					  SUGGESTIONS_DISMISS,
 | 
				
			||||||
} from '../actions/suggestions';
 | 
					} from '../actions/suggestions';
 | 
				
			||||||
import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'mastodon/actions/accounts';
 | 
					import { ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS } from 'flavours/glitch/actions/accounts';
 | 
				
			||||||
import { DOMAIN_BLOCK_SUCCESS } from 'mastodon/actions/domain_blocks';
 | 
					import { DOMAIN_BLOCK_SUCCESS } from 'flavours/glitch/actions/domain_blocks';
 | 
				
			||||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
 | 
					import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const initialState = ImmutableMap({
 | 
					const initialState = ImmutableMap({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,14 @@ import {
 | 
				
			||||||
  MUTES_FETCH_SUCCESS,
 | 
					  MUTES_FETCH_SUCCESS,
 | 
				
			||||||
  MUTES_EXPAND_SUCCESS,
 | 
					  MUTES_EXPAND_SUCCESS,
 | 
				
			||||||
} from 'flavours/glitch/actions/mutes';
 | 
					} from 'flavours/glitch/actions/mutes';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DIRECTORY_FETCH_REQUEST,
 | 
				
			||||||
 | 
					  DIRECTORY_FETCH_SUCCESS,
 | 
				
			||||||
 | 
					  DIRECTORY_FETCH_FAIL,
 | 
				
			||||||
 | 
					  DIRECTORY_EXPAND_REQUEST,
 | 
				
			||||||
 | 
					  DIRECTORY_EXPAND_SUCCESS,
 | 
				
			||||||
 | 
					  DIRECTORY_EXPAND_FAIL,
 | 
				
			||||||
 | 
					} from 'flavours/glitch/actions/directory';
 | 
				
			||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 | 
					import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const initialState = ImmutableMap({
 | 
					const initialState = ImmutableMap({
 | 
				
			||||||
| 
						 | 
					@ -74,6 +82,16 @@ export default function userLists(state = initialState, action) {
 | 
				
			||||||
    return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
 | 
					    return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
 | 
				
			||||||
  case MUTES_EXPAND_SUCCESS:
 | 
					  case MUTES_EXPAND_SUCCESS:
 | 
				
			||||||
    return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
 | 
					    return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
 | 
				
			||||||
 | 
					  case DIRECTORY_FETCH_SUCCESS:
 | 
				
			||||||
 | 
					    return state.setIn(['directory', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false);
 | 
				
			||||||
 | 
					  case DIRECTORY_EXPAND_SUCCESS:
 | 
				
			||||||
 | 
					    return state.updateIn(['directory', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false);
 | 
				
			||||||
 | 
					  case DIRECTORY_FETCH_REQUEST:
 | 
				
			||||||
 | 
					  case DIRECTORY_EXPAND_REQUEST:
 | 
				
			||||||
 | 
					    return state.setIn(['directory', 'isLoading'], true);
 | 
				
			||||||
 | 
					  case DIRECTORY_FETCH_FAIL:
 | 
				
			||||||
 | 
					  case DIRECTORY_EXPAND_FAIL:
 | 
				
			||||||
 | 
					    return state.setIn(['directory', 'isLoading'], false);
 | 
				
			||||||
  default:
 | 
					  default:
 | 
				
			||||||
    return state;
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -415,6 +415,24 @@
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.directory__section-headline {
 | 
				
			||||||
 | 
					    background: darken($ui-base-color, 2%);
 | 
				
			||||||
 | 
					    border-bottom-color: transparent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    a,
 | 
				
			||||||
 | 
					    button {
 | 
				
			||||||
 | 
					      &.active {
 | 
				
			||||||
 | 
					        &::before {
 | 
				
			||||||
 | 
					          display: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &::after {
 | 
				
			||||||
 | 
					          border-color: transparent transparent darken($ui-base-color, 7%);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.account__moved-note {
 | 
					.account__moved-note {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										180
									
								
								app/javascript/flavours/glitch/styles/components/directory.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								app/javascript/flavours/glitch/styles/components/directory.scss
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,180 @@
 | 
				
			||||||
 | 
					.directory {
 | 
				
			||||||
 | 
					  &__list {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					    margin: 10px 0;
 | 
				
			||||||
 | 
					    transition: opacity 100ms ease-in;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.loading {
 | 
				
			||||||
 | 
					      opacity: 0.7;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @media screen and (max-width: $no-gap-breakpoint) {
 | 
				
			||||||
 | 
					      margin: 0;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &__card {
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__img {
 | 
				
			||||||
 | 
					      height: 125px;
 | 
				
			||||||
 | 
					      position: relative;
 | 
				
			||||||
 | 
					      background: darken($ui-base-color, 12%);
 | 
				
			||||||
 | 
					      overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      img {
 | 
				
			||||||
 | 
					        display: block;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					        margin: 0;
 | 
				
			||||||
 | 
					        object-fit: cover;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__bar {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      background: lighten($ui-base-color, 4%);
 | 
				
			||||||
 | 
					      padding: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &__name {
 | 
				
			||||||
 | 
					        flex: 1 1 auto;
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					        text-decoration: none;
 | 
				
			||||||
 | 
					        overflow: hidden;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &__relationship {
 | 
				
			||||||
 | 
					        width: 23px;
 | 
				
			||||||
 | 
					        min-height: 1px;
 | 
				
			||||||
 | 
					        flex: 0 0 auto;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .avatar {
 | 
				
			||||||
 | 
					        flex: 0 0 auto;
 | 
				
			||||||
 | 
					        width: 48px;
 | 
				
			||||||
 | 
					        height: 48px;
 | 
				
			||||||
 | 
					        padding-top: 2px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        img {
 | 
				
			||||||
 | 
					          width: 100%;
 | 
				
			||||||
 | 
					          height: 100%;
 | 
				
			||||||
 | 
					          display: block;
 | 
				
			||||||
 | 
					          margin: 0;
 | 
				
			||||||
 | 
					          border-radius: 4px;
 | 
				
			||||||
 | 
					          background: darken($ui-base-color, 8%);
 | 
				
			||||||
 | 
					          object-fit: cover;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .display-name {
 | 
				
			||||||
 | 
					        margin-left: 15px;
 | 
				
			||||||
 | 
					        text-align: left;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        strong {
 | 
				
			||||||
 | 
					          font-size: 15px;
 | 
				
			||||||
 | 
					          color: $primary-text-color;
 | 
				
			||||||
 | 
					          font-weight: 500;
 | 
				
			||||||
 | 
					          overflow: hidden;
 | 
				
			||||||
 | 
					          text-overflow: ellipsis;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        span {
 | 
				
			||||||
 | 
					          display: block;
 | 
				
			||||||
 | 
					          font-size: 14px;
 | 
				
			||||||
 | 
					          color: $darker-text-color;
 | 
				
			||||||
 | 
					          font-weight: 400;
 | 
				
			||||||
 | 
					          overflow: hidden;
 | 
				
			||||||
 | 
					          text-overflow: ellipsis;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__extra {
 | 
				
			||||||
 | 
					      background: $ui-base-color;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .accounts-table__count {
 | 
				
			||||||
 | 
					        width: 33.33%;
 | 
				
			||||||
 | 
					        flex: 0 0 auto;
 | 
				
			||||||
 | 
					        padding: 15px 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .account__header__content {
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        padding: 15px 10px;
 | 
				
			||||||
 | 
					        border-bottom: 1px solid lighten($ui-base-color, 8%);
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        min-height: 18px + 30px;
 | 
				
			||||||
 | 
					        white-space: nowrap;
 | 
				
			||||||
 | 
					        overflow: hidden;
 | 
				
			||||||
 | 
					        text-overflow: ellipsis;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        p {
 | 
				
			||||||
 | 
					          display: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          &:first-child {
 | 
				
			||||||
 | 
					            display: inline;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        br {
 | 
				
			||||||
 | 
					          display: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.filter-form {
 | 
				
			||||||
 | 
					  background: $ui-base-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &__column {
 | 
				
			||||||
 | 
					    padding: 10px 15px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .radio-button {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.radio-button {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					  padding: 6px 0;
 | 
				
			||||||
 | 
					  line-height: 18px;
 | 
				
			||||||
 | 
					  cursor: default;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  input[type=radio],
 | 
				
			||||||
 | 
					  input[type=checkbox] {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &__input {
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    border: 1px solid $ui-primary-color;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    width: 18px;
 | 
				
			||||||
 | 
					    height: 18px;
 | 
				
			||||||
 | 
					    flex: 0 0 auto;
 | 
				
			||||||
 | 
					    margin-right: 10px;
 | 
				
			||||||
 | 
					    top: -1px;
 | 
				
			||||||
 | 
					    border-radius: 50%;
 | 
				
			||||||
 | 
					    vertical-align: middle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.checked {
 | 
				
			||||||
 | 
					      border-color: lighten($ui-highlight-color, 8%);
 | 
				
			||||||
 | 
					      background: lighten($ui-highlight-color, 8%);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1467,6 +1467,7 @@ noscript {
 | 
				
			||||||
@import 'composer';
 | 
					@import 'composer';
 | 
				
			||||||
@import 'columns';
 | 
					@import 'columns';
 | 
				
			||||||
@import 'regeneration_indicator';
 | 
					@import 'regeneration_indicator';
 | 
				
			||||||
 | 
					@import 'directory';
 | 
				
			||||||
@import 'search';
 | 
					@import 'search';
 | 
				
			||||||
@import 'emoji';
 | 
					@import 'emoji';
 | 
				
			||||||
@import 'doodle';
 | 
					@import 'doodle';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -669,38 +669,13 @@
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
&.detailed,
 | 
					  &.detailed,
 | 
				
			||||||
&.fullscreen {
 | 
					  &.fullscreen {
 | 
				
			||||||
  .video-player__buttons {
 | 
					    .video-player__buttons {
 | 
				
			||||||
    button {
 | 
					      button {
 | 
				
			||||||
      padding-top: 10px;
 | 
					        padding-top: 10px;
 | 
				
			||||||
      padding-bottom: 10px;
 | 
					        padding-bottom: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.media-spoiler-video {
 | 
					 | 
				
			||||||
  background-size: cover;
 | 
					 | 
				
			||||||
  background-repeat: no-repeat;
 | 
					 | 
				
			||||||
  background-position: center;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
  margin-top: 8px;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @include fullwidth-gallery;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  border: 0;
 | 
					 | 
				
			||||||
  display: block;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.media-spoiler-video-play-icon {
 | 
					 | 
				
			||||||
  border-radius: 100px;
 | 
					 | 
				
			||||||
  color: rgba($primary-text-color, 0.8);
 | 
					 | 
				
			||||||
  font-size: 36px;
 | 
					 | 
				
			||||||
  left: 50%;
 | 
					 | 
				
			||||||
  padding: 5px;
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  top: 50%;
 | 
					 | 
				
			||||||
  transform: translate(-50%, -50%);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -83,6 +83,24 @@
 | 
				
			||||||
    padding: 0;
 | 
					    padding: 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .directory__list {
 | 
				
			||||||
 | 
					    display: grid;
 | 
				
			||||||
 | 
					    grid-gap: 10px;
 | 
				
			||||||
 | 
					    grid-template-columns: minmax(0, 50%) minmax(0, 50%);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @media screen and (max-width: $no-gap-breakpoint) {
 | 
				
			||||||
 | 
					      display: block;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .directory__card {
 | 
				
			||||||
 | 
					    margin-bottom: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .filter-form {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .autosuggest-textarea__textarea {
 | 
					  .autosuggest-textarea__textarea {
 | 
				
			||||||
    font-size: 16px;
 | 
					    font-size: 16px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -886,67 +886,6 @@ a.status-card.compact:hover {
 | 
				
			||||||
  background-position: center center;
 | 
					  background-position: center center;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.status__video-player {
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  background: $base-shadow-color;
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
  cursor: default; /* May not be needed */
 | 
					 | 
				
			||||||
  margin-top: 8px;
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @include fullwidth-gallery;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status__video-player-video {
 | 
					 | 
				
			||||||
  height: 100%;
 | 
					 | 
				
			||||||
  object-fit: contain;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  top: 50%;
 | 
					 | 
				
			||||||
  transform: translateY(-50%);
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  z-index: 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &:not(.letterbox) {
 | 
					 | 
				
			||||||
    height: 100%;
 | 
					 | 
				
			||||||
    object-fit: cover;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status__video-player-expand,
 | 
					 | 
				
			||||||
.status__video-player-mute {
 | 
					 | 
				
			||||||
  color: $primary-text-color;
 | 
					 | 
				
			||||||
  opacity: 0.8;
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  right: 4px;
 | 
					 | 
				
			||||||
  text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status__video-player-spoiler {
 | 
					 | 
				
			||||||
  display: none;
 | 
					 | 
				
			||||||
  color: $primary-text-color;
 | 
					 | 
				
			||||||
  left: 4px;
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color;
 | 
					 | 
				
			||||||
  top: 4px;
 | 
					 | 
				
			||||||
  z-index: 100;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.status__video-player-spoiler--visible {
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status__video-player-expand {
 | 
					 | 
				
			||||||
  bottom: 4px;
 | 
					 | 
				
			||||||
  z-index: 100;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status__video-player-mute {
 | 
					 | 
				
			||||||
  top: 4px;
 | 
					 | 
				
			||||||
  z-index: 5;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.attachment-list {
 | 
					.attachment-list {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
  font-size: 14px;
 | 
					  font-size: 14px;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -769,6 +769,24 @@
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .directory__list {
 | 
				
			||||||
 | 
					    display: grid;
 | 
				
			||||||
 | 
					    grid-gap: 10px;
 | 
				
			||||||
 | 
					    grid-template-columns: minmax(0, 50%) minmax(0, 50%);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @media screen and (max-width: $no-gap-breakpoint) {
 | 
				
			||||||
 | 
					      display: block;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .icon-button {
 | 
				
			||||||
 | 
					      font-size: 18px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .directory__card {
 | 
				
			||||||
 | 
					    margin-bottom: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .card-grid {
 | 
					  .card-grid {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex-wrap: wrap;
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -161,3 +161,7 @@ export function Search () {
 | 
				
			||||||
export function Tesseract () {
 | 
					export function Tesseract () {
 | 
				
			||||||
  return import(/*webpackChunkName: "tesseract" */'tesseract.js');
 | 
					  return import(/*webpackChunkName: "tesseract" */'tesseract.js');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Directory () {
 | 
				
			||||||
 | 
					  return import(/* webpackChunkName: "features/glitch/async/directory" */'flavours/glitch/features/directory');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,3 +5,5 @@ export const termsLink = '/terms';
 | 
				
			||||||
export const accountAdminLink = (id) => `/admin/accounts/${id}`;
 | 
					export const accountAdminLink = (id) => `/admin/accounts/${id}`;
 | 
				
			||||||
export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses/${status_id}`;
 | 
					export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses/${status_id}`;
 | 
				
			||||||
export const filterEditLink = (id) => `/filters/${id}/edit`;
 | 
					export const filterEditLink = (id) => `/filters/${id}/edit`;
 | 
				
			||||||
 | 
					export const relationshipsLink = '/relationships';
 | 
				
			||||||
 | 
					export const securityLink = '/auth/edit';
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,6 +26,7 @@ export const pollLimits = (initialState && initialState.poll_limits);
 | 
				
			||||||
export const invitesEnabled = getMeta('invites_enabled');
 | 
					export const invitesEnabled = getMeta('invites_enabled');
 | 
				
			||||||
export const version = getMeta('version');
 | 
					export const version = getMeta('version');
 | 
				
			||||||
export const mascot = getMeta('mascot');
 | 
					export const mascot = getMeta('mascot');
 | 
				
			||||||
 | 
					export const profile_directory = getMeta('profile_directory');
 | 
				
			||||||
export const isStaff = getMeta('is_staff');
 | 
					export const isStaff = getMeta('is_staff');
 | 
				
			||||||
export const defaultContentType = getMeta('default_content_type');
 | 
					export const defaultContentType = getMeta('default_content_type');
 | 
				
			||||||
export const forceSingleColumn = getMeta('advanced_layout') === false;
 | 
					export const forceSingleColumn = getMeta('advanced_layout') === false;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										61
									
								
								app/javascript/mastodon/actions/directory.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								app/javascript/mastodon/actions/directory.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,61 @@
 | 
				
			||||||
 | 
					import api from '../api';
 | 
				
			||||||
 | 
					import { importFetchedAccounts } from './importer';
 | 
				
			||||||
 | 
					import { fetchRelationships } from './accounts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
 | 
				
			||||||
 | 
					export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
 | 
				
			||||||
 | 
					export const DIRECTORY_FETCH_FAIL    = 'DIRECTORY_FETCH_FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
 | 
				
			||||||
 | 
					export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
 | 
				
			||||||
 | 
					export const DIRECTORY_EXPAND_FAIL    = 'DIRECTORY_EXPAND_FAIL';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fetchDirectory = params => (dispatch, getState) => {
 | 
				
			||||||
 | 
					  dispatch(fetchDirectoryRequest());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  api(getState).get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
 | 
				
			||||||
 | 
					    dispatch(importFetchedAccounts(data));
 | 
				
			||||||
 | 
					    dispatch(fetchDirectorySuccess(data));
 | 
				
			||||||
 | 
					    dispatch(fetchRelationships(data.map(x => x.id)));
 | 
				
			||||||
 | 
					  }).catch(error => dispatch(fetchDirectoryFail(error)));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fetchDirectoryRequest = () => ({
 | 
				
			||||||
 | 
					  type: DIRECTORY_FETCH_REQUEST,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fetchDirectorySuccess = accounts => ({
 | 
				
			||||||
 | 
					  type: DIRECTORY_FETCH_SUCCESS,
 | 
				
			||||||
 | 
					  accounts,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const fetchDirectoryFail = error => ({
 | 
				
			||||||
 | 
					  type: DIRECTORY_FETCH_FAIL,
 | 
				
			||||||
 | 
					  error,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const expandDirectory = params => (dispatch, getState) => {
 | 
				
			||||||
 | 
					  dispatch(expandDirectoryRequest());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  api(getState).get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
 | 
				
			||||||
 | 
					    dispatch(importFetchedAccounts(data));
 | 
				
			||||||
 | 
					    dispatch(expandDirectorySuccess(data));
 | 
				
			||||||
 | 
					    dispatch(fetchRelationships(data.map(x => x.id)));
 | 
				
			||||||
 | 
					  }).catch(error => dispatch(expandDirectoryFail(error)));
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const expandDirectoryRequest = () => ({
 | 
				
			||||||
 | 
					  type: DIRECTORY_EXPAND_REQUEST,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const expandDirectorySuccess = accounts => ({
 | 
				
			||||||
 | 
					  type: DIRECTORY_EXPAND_SUCCESS,
 | 
				
			||||||
 | 
					  accounts,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const expandDirectoryFail = error => ({
 | 
				
			||||||
 | 
					  type: DIRECTORY_EXPAND_FAIL,
 | 
				
			||||||
 | 
					  error,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										35
									
								
								app/javascript/mastodon/components/radio_button.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								app/javascript/mastodon/components/radio_button.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,35 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class RadioButton extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static propTypes = {
 | 
				
			||||||
 | 
					    value: PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    checked: PropTypes.bool,
 | 
				
			||||||
 | 
					    name: PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    onChange: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    label: PropTypes.node.isRequired,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { name, value, checked, onChange, label } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <label className='radio-button'>
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          name={name}
 | 
				
			||||||
 | 
					          type='radio'
 | 
				
			||||||
 | 
					          value={value}
 | 
				
			||||||
 | 
					          checked={checked}
 | 
				
			||||||
 | 
					          onChange={onChange}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <span className={classNames('radio-button__input', { checked })} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <span>{label}</span>
 | 
				
			||||||
 | 
					      </label>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,149 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
				
			||||||
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import { makeGetAccount } from 'mastodon/selectors';
 | 
				
			||||||
 | 
					import Avatar from 'mastodon/components/avatar';
 | 
				
			||||||
 | 
					import DisplayName from 'mastodon/components/display_name';
 | 
				
			||||||
 | 
					import Permalink from 'mastodon/components/permalink';
 | 
				
			||||||
 | 
					import RelativeTimestamp from 'mastodon/components/relative_timestamp';
 | 
				
			||||||
 | 
					import IconButton from 'mastodon/components/icon_button';
 | 
				
			||||||
 | 
					import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
 | 
				
			||||||
 | 
					import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
 | 
				
			||||||
 | 
					import { shortNumberFormat } from 'mastodon/utils/numbers';
 | 
				
			||||||
 | 
					import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'mastodon/actions/accounts';
 | 
				
			||||||
 | 
					import { openModal } from 'mastodon/actions/modal';
 | 
				
			||||||
 | 
					import { initMuteModal } from 'mastodon/actions/mutes';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  follow: { id: 'account.follow', defaultMessage: 'Follow' },
 | 
				
			||||||
 | 
					  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
 | 
				
			||||||
 | 
					  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
 | 
				
			||||||
 | 
					  unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
 | 
				
			||||||
 | 
					  unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const makeMapStateToProps = () => {
 | 
				
			||||||
 | 
					  const getAccount = makeGetAccount();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const mapStateToProps = (state, { id }) => ({
 | 
				
			||||||
 | 
					    account: getAccount(state, id),
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return mapStateToProps;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapDispatchToProps = (dispatch, { intl }) => ({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onFollow (account) {
 | 
				
			||||||
 | 
					    if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
 | 
				
			||||||
 | 
					      if (unfollowModal) {
 | 
				
			||||||
 | 
					        dispatch(openModal('CONFIRM', {
 | 
				
			||||||
 | 
					          message: <FormattedMessage id='confirmations.unfollow.message' defaultMessage='Are you sure you want to unfollow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
 | 
				
			||||||
 | 
					          confirm: intl.formatMessage(messages.unfollowConfirm),
 | 
				
			||||||
 | 
					          onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
 | 
				
			||||||
 | 
					        }));
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        dispatch(unfollowAccount(account.get('id')));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      dispatch(followAccount(account.get('id')));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onBlock (account) {
 | 
				
			||||||
 | 
					    if (account.getIn(['relationship', 'blocking'])) {
 | 
				
			||||||
 | 
					      dispatch(unblockAccount(account.get('id')));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      dispatch(blockAccount(account.get('id')));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onMute (account) {
 | 
				
			||||||
 | 
					    if (account.getIn(['relationship', 'muting'])) {
 | 
				
			||||||
 | 
					      dispatch(unmuteAccount(account.get('id')));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      dispatch(initMuteModal(account));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default @injectIntl
 | 
				
			||||||
 | 
					@connect(makeMapStateToProps, mapDispatchToProps)
 | 
				
			||||||
 | 
					class AccountCard extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static propTypes = {
 | 
				
			||||||
 | 
					    account: ImmutablePropTypes.map.isRequired,
 | 
				
			||||||
 | 
					    intl: PropTypes.object.isRequired,
 | 
				
			||||||
 | 
					    onFollow: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    onBlock: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    onMute: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleFollow = () => {
 | 
				
			||||||
 | 
					    this.props.onFollow(this.props.account);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleBlock = () => {
 | 
				
			||||||
 | 
					    this.props.onBlock(this.props.account);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleMute = () => {
 | 
				
			||||||
 | 
					    this.props.onMute(this.props.account);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { account, intl } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let buttons;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (account.get('id') !== me && account.get('relationship', null) !== null) {
 | 
				
			||||||
 | 
					      const following = account.getIn(['relationship', 'following']);
 | 
				
			||||||
 | 
					      const requested = account.getIn(['relationship', 'requested']);
 | 
				
			||||||
 | 
					      const blocking  = account.getIn(['relationship', 'blocking']);
 | 
				
			||||||
 | 
					      const muting    = account.getIn(['relationship', 'muting']);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (requested) {
 | 
				
			||||||
 | 
					        buttons = <IconButton disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />;
 | 
				
			||||||
 | 
					      } else if (blocking) {
 | 
				
			||||||
 | 
					        buttons = <IconButton active icon='unlock' title={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.handleBlock} />;
 | 
				
			||||||
 | 
					      } else if (muting) {
 | 
				
			||||||
 | 
					        buttons = <IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />;
 | 
				
			||||||
 | 
					      } else if (!account.get('moved') || following) {
 | 
				
			||||||
 | 
					        buttons = <IconButton icon={following ? 'user-times' : 'user-plus'} title={intl.formatMessage(following ? messages.unfollow : messages.follow)} onClick={this.handleFollow} active={following} />;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className='directory__card'>
 | 
				
			||||||
 | 
					        <div className='directory__card__img'>
 | 
				
			||||||
 | 
					          <img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className='directory__card__bar'>
 | 
				
			||||||
 | 
					          <Permalink className='directory__card__bar__name' href={account.get('url')} to={`/accounts/${account.get('id')}`}>
 | 
				
			||||||
 | 
					            <Avatar account={account} size={48} />
 | 
				
			||||||
 | 
					            <DisplayName account={account} />
 | 
				
			||||||
 | 
					          </Permalink>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className='directory__card__bar__relationship account__relationship'>
 | 
				
			||||||
 | 
					            {buttons}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className='directory__card__extra'>
 | 
				
			||||||
 | 
					          <div className='account__header__content' dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className='directory__card__extra'>
 | 
				
			||||||
 | 
					          <div className='accounts-table__count'>{shortNumberFormat(account.get('statuses_count'))} <small><FormattedMessage id='account.posts' defaultMessage='Toots' /></small></div>
 | 
				
			||||||
 | 
					          <div className='accounts-table__count'>{shortNumberFormat(account.get('followers_count'))} <small><FormattedMessage id='account.followers' defaultMessage='Followers' /></small></div>
 | 
				
			||||||
 | 
					          <div className='accounts-table__count'>{account.get('last_status_at') === null ? <FormattedMessage id='account.never_active' defaultMessage='Never' /> : <RelativeTimestamp timestamp={account.get('last_status_at')} />} <small><FormattedMessage id='account.last_status' defaultMessage='Last active' /></small></div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										171
									
								
								app/javascript/mastodon/features/directory/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										171
									
								
								app/javascript/mastodon/features/directory/index.js
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,171 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import { defineMessages, injectIntl } from 'react-intl';
 | 
				
			||||||
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
 | 
					import Column from 'mastodon/components/column';
 | 
				
			||||||
 | 
					import ColumnHeader from 'mastodon/components/column_header';
 | 
				
			||||||
 | 
					import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'mastodon/actions/columns';
 | 
				
			||||||
 | 
					import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
 | 
				
			||||||
 | 
					import { List as ImmutableList } from 'immutable';
 | 
				
			||||||
 | 
					import AccountCard from './components/account_card';
 | 
				
			||||||
 | 
					import RadioButton from 'mastodon/components/radio_button';
 | 
				
			||||||
 | 
					import classNames from 'classnames';
 | 
				
			||||||
 | 
					import LoadMore from 'mastodon/components/load_more';
 | 
				
			||||||
 | 
					import { ScrollContainer } from 'react-router-scroll-4';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
 | 
				
			||||||
 | 
					  recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' },
 | 
				
			||||||
 | 
					  newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
 | 
				
			||||||
 | 
					  local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
 | 
				
			||||||
 | 
					  federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
 | 
					  accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()),
 | 
				
			||||||
 | 
					  isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true),
 | 
				
			||||||
 | 
					  domain: state.getIn(['meta', 'domain']),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default @connect(mapStateToProps)
 | 
				
			||||||
 | 
					@injectIntl
 | 
				
			||||||
 | 
					class Directory extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static contextTypes = {
 | 
				
			||||||
 | 
					    router: PropTypes.object,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static propTypes = {
 | 
				
			||||||
 | 
					    isLoading: PropTypes.bool,
 | 
				
			||||||
 | 
					    accountIds: ImmutablePropTypes.list.isRequired,
 | 
				
			||||||
 | 
					    dispatch: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    shouldUpdateScroll: PropTypes.func,
 | 
				
			||||||
 | 
					    columnId: PropTypes.string,
 | 
				
			||||||
 | 
					    intl: PropTypes.object.isRequired,
 | 
				
			||||||
 | 
					    multiColumn: PropTypes.bool,
 | 
				
			||||||
 | 
					    domain: PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    params: PropTypes.shape({
 | 
				
			||||||
 | 
					      order: PropTypes.string,
 | 
				
			||||||
 | 
					      local: PropTypes.bool,
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  state = {
 | 
				
			||||||
 | 
					    order: null,
 | 
				
			||||||
 | 
					    local: null,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handlePin = () => {
 | 
				
			||||||
 | 
					    const { columnId, dispatch } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (columnId) {
 | 
				
			||||||
 | 
					      dispatch(removeColumn(columnId));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      dispatch(addColumn('DIRECTORY', this.getParams(this.props, this.state)));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getParams = (props, state) => ({
 | 
				
			||||||
 | 
					    order: state.order === null ? (props.params.order || 'active') : state.order,
 | 
				
			||||||
 | 
					    local: state.local === null ? (props.params.local || false) : state.local,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleMove = dir => {
 | 
				
			||||||
 | 
					    const { columnId, dispatch } = this.props;
 | 
				
			||||||
 | 
					    dispatch(moveColumn(columnId, dir));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleHeaderClick = () => {
 | 
				
			||||||
 | 
					    this.column.scrollTop();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  componentDidMount () {
 | 
				
			||||||
 | 
					    const { dispatch } = this.props;
 | 
				
			||||||
 | 
					    dispatch(fetchDirectory(this.getParams(this.props, this.state)));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  componentDidUpdate (prevProps, prevState) {
 | 
				
			||||||
 | 
					    const { dispatch } = this.props;
 | 
				
			||||||
 | 
					    const paramsOld = this.getParams(prevProps, prevState);
 | 
				
			||||||
 | 
					    const paramsNew = this.getParams(this.props, this.state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) {
 | 
				
			||||||
 | 
					      dispatch(fetchDirectory(paramsNew));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setRef = c => {
 | 
				
			||||||
 | 
					    this.column = c;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleChangeOrder = e => {
 | 
				
			||||||
 | 
					    const { dispatch, columnId } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (columnId) {
 | 
				
			||||||
 | 
					      dispatch(changeColumnParams(columnId, ['order'], e.target.value));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      this.setState({ order: e.target.value });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleChangeLocal = e => {
 | 
				
			||||||
 | 
					    const { dispatch, columnId } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (columnId) {
 | 
				
			||||||
 | 
					      dispatch(changeColumnParams(columnId, ['local'], e.target.value === '1'));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      this.setState({ local: e.target.value === '1' });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleLoadMore = () => {
 | 
				
			||||||
 | 
					    const { dispatch } = this.props;
 | 
				
			||||||
 | 
					    dispatch(expandDirectory(this.getParams(this.props, this.state)));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { isLoading, accountIds, intl, columnId, multiColumn, domain, shouldUpdateScroll } = this.props;
 | 
				
			||||||
 | 
					    const { order, local }  = this.getParams(this.props, this.state);
 | 
				
			||||||
 | 
					    const pinned = !!columnId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const scrollableArea = (
 | 
				
			||||||
 | 
					      <div className='scrollable' style={{ background: 'transparent' }}>
 | 
				
			||||||
 | 
					        <div className='filter-form'>
 | 
				
			||||||
 | 
					          <div className='filter-form__column' role='group'>
 | 
				
			||||||
 | 
					            <RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} />
 | 
				
			||||||
 | 
					            <RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={this.handleChangeOrder} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          <div className='filter-form__column' role='group'>
 | 
				
			||||||
 | 
					            <RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain })} checked={local} onChange={this.handleChangeLocal} />
 | 
				
			||||||
 | 
					            <RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={this.handleChangeLocal} />
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div className={classNames('directory__list', { loading: isLoading })}>
 | 
				
			||||||
 | 
					          {accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <LoadMore onClick={this.handleLoadMore} visible={!isLoading} />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
 | 
				
			||||||
 | 
					        <ColumnHeader
 | 
				
			||||||
 | 
					          icon='address-book-o'
 | 
				
			||||||
 | 
					          title={intl.formatMessage(messages.title)}
 | 
				
			||||||
 | 
					          onPin={this.handlePin}
 | 
				
			||||||
 | 
					          onMove={this.handleMove}
 | 
				
			||||||
 | 
					          onClick={this.handleHeaderClick}
 | 
				
			||||||
 | 
					          pinned={pinned}
 | 
				
			||||||
 | 
					          multiColumn={multiColumn}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {multiColumn && !pinned ? <ScrollContainer scrollKey='directory' shouldUpdateScroll={shouldUpdateScroll}>{scrollableArea}</ScrollContainer> : scrollableArea}
 | 
				
			||||||
 | 
					      </Column>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -107,7 +107,7 @@ class GettingStarted extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (profile_directory) {
 | 
					      if (profile_directory) {
 | 
				
			||||||
        navItems.push(
 | 
					        navItems.push(
 | 
				
			||||||
          <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} href='/explore' />
 | 
					          <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        height += 48;
 | 
					        height += 48;
 | 
				
			||||||
| 
						 | 
					@ -120,7 +120,7 @@ class GettingStarted extends ImmutablePureComponent {
 | 
				
			||||||
      height += 34;
 | 
					      height += 34;
 | 
				
			||||||
    } else if (profile_directory) {
 | 
					    } else if (profile_directory) {
 | 
				
			||||||
      navItems.push(
 | 
					      navItems.push(
 | 
				
			||||||
        <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} href='/explore' />
 | 
					        <ColumnLink key={i++} icon='address-book' text={intl.formatMessage(messages.profile_directory)} to='/directory' />
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      height += 48;
 | 
					      height += 48;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,7 +20,7 @@ const mapDispatchToProps = (dispatch, { columnId }) => ({
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onLoad (value) {
 | 
					  onLoad (value) {
 | 
				
			||||||
    return api().get('/api/v2/search', { params: { q: value } }).then(response => {
 | 
					    return api().get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
 | 
				
			||||||
      return (response.data.hashtags || []).map((tag) => {
 | 
					      return (response.data.hashtags || []).map((tag) => {
 | 
				
			||||||
        return { value: tag.name, label: `#${tag.name}` };
 | 
					        return { value: tag.name, label: `#${tag.name}` };
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -84,28 +84,38 @@ const makeMapStateToProps = () => {
 | 
				
			||||||
  const getDescendantsIds = createSelector([
 | 
					  const getDescendantsIds = createSelector([
 | 
				
			||||||
    (_, { id }) => id,
 | 
					    (_, { id }) => id,
 | 
				
			||||||
    state => state.getIn(['contexts', 'replies']),
 | 
					    state => state.getIn(['contexts', 'replies']),
 | 
				
			||||||
  ], (statusId, contextReplies) => {
 | 
					    state => state.get('statuses'),
 | 
				
			||||||
    let descendantsIds = Immutable.List();
 | 
					  ], (statusId, contextReplies, statuses) => {
 | 
				
			||||||
    descendantsIds = descendantsIds.withMutations(mutable => {
 | 
					    let descendantsIds = [];
 | 
				
			||||||
      const ids = [statusId];
 | 
					    const ids = [statusId];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      while (ids.length > 0) {
 | 
					    while (ids.length > 0) {
 | 
				
			||||||
        let id        = ids.shift();
 | 
					      let id        = ids.shift();
 | 
				
			||||||
        const replies = contextReplies.get(id);
 | 
					      const replies = contextReplies.get(id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (statusId !== id) {
 | 
					      if (statusId !== id) {
 | 
				
			||||||
          mutable.push(id);
 | 
					        descendantsIds.push(id);
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (replies) {
 | 
					 | 
				
			||||||
          replies.reverse().forEach(reply => {
 | 
					 | 
				
			||||||
            ids.unshift(reply);
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return descendantsIds;
 | 
					      if (replies) {
 | 
				
			||||||
 | 
					        replies.reverse().forEach(reply => {
 | 
				
			||||||
 | 
					          ids.unshift(reply);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account'));
 | 
				
			||||||
 | 
					    if (insertAt !== -1) {
 | 
				
			||||||
 | 
					      descendantsIds.forEach((id, idx) => {
 | 
				
			||||||
 | 
					        if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) {
 | 
				
			||||||
 | 
					          descendantsIds.splice(idx, 1);
 | 
				
			||||||
 | 
					          descendantsIds.splice(insertAt, 0, id);
 | 
				
			||||||
 | 
					          insertAt += 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Immutable.List(descendantsIds);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const mapStateToProps = (state, props) => {
 | 
					  const mapStateToProps = (state, props) => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,18 @@ import BundleContainer from '../containers/bundle_container';
 | 
				
			||||||
import ColumnLoading from './column_loading';
 | 
					import ColumnLoading from './column_loading';
 | 
				
			||||||
import DrawerLoading from './drawer_loading';
 | 
					import DrawerLoading from './drawer_loading';
 | 
				
			||||||
import BundleColumnError from './bundle_column_error';
 | 
					import BundleColumnError from './bundle_column_error';
 | 
				
			||||||
import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from '../../ui/util/async-components';
 | 
					import {
 | 
				
			||||||
 | 
					  Compose,
 | 
				
			||||||
 | 
					  Notifications,
 | 
				
			||||||
 | 
					  HomeTimeline,
 | 
				
			||||||
 | 
					  CommunityTimeline,
 | 
				
			||||||
 | 
					  PublicTimeline,
 | 
				
			||||||
 | 
					  HashtagTimeline,
 | 
				
			||||||
 | 
					  DirectTimeline,
 | 
				
			||||||
 | 
					  FavouritedStatuses,
 | 
				
			||||||
 | 
					  ListTimeline,
 | 
				
			||||||
 | 
					  Directory,
 | 
				
			||||||
 | 
					} from '../../ui/util/async-components';
 | 
				
			||||||
import Icon from 'mastodon/components/icon';
 | 
					import Icon from 'mastodon/components/icon';
 | 
				
			||||||
import ComposePanel from './compose_panel';
 | 
					import ComposePanel from './compose_panel';
 | 
				
			||||||
import NavigationPanel from './navigation_panel';
 | 
					import NavigationPanel from './navigation_panel';
 | 
				
			||||||
| 
						 | 
					@ -30,6 +41,7 @@ const componentMap = {
 | 
				
			||||||
  'DIRECT': DirectTimeline,
 | 
					  'DIRECT': DirectTimeline,
 | 
				
			||||||
  'FAVOURITES': FavouritedStatuses,
 | 
					  'FAVOURITES': FavouritedStatuses,
 | 
				
			||||||
  'LIST': ListTimeline,
 | 
					  'LIST': ListTimeline,
 | 
				
			||||||
 | 
					  'DIRECTORY': Directory,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const messages = defineMessages({
 | 
					const messages = defineMessages({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,7 @@ const NavigationPanel = () => (
 | 
				
			||||||
    <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
 | 
					    <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
 | 
				
			||||||
    <NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
 | 
					    <NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
 | 
				
			||||||
    <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
 | 
					    <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
 | 
				
			||||||
 | 
					    {profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.profile_directory' defaultMessage='Profile directory' /></NavLink>}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <ListPanel />
 | 
					    <ListPanel />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,7 +26,6 @@ const NavigationPanel = () => (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <a className='column-link column-link--transparent' href='/settings/preferences'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>
 | 
					    <a className='column-link column-link--transparent' href='/settings/preferences'><Icon className='column-link__icon' id='cog' fixedWidth /><FormattedMessage id='navigation_bar.preferences' defaultMessage='Preferences' /></a>
 | 
				
			||||||
    <a className='column-link column-link--transparent' href='/relationships'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>
 | 
					    <a className='column-link column-link--transparent' href='/relationships'><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='navigation_bar.follows_and_followers' defaultMessage='Follows and followers' /></a>
 | 
				
			||||||
    {!!profile_directory && <a className='column-link column-link--transparent' href='/explore'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></a>}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {showTrends && <div className='flex-spacer' />}
 | 
					    {showTrends && <div className='flex-spacer' />}
 | 
				
			||||||
    {showTrends && <TrendsContainer />}
 | 
					    {showTrends && <TrendsContainer />}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -47,6 +47,7 @@ import {
 | 
				
			||||||
  PinnedStatuses,
 | 
					  PinnedStatuses,
 | 
				
			||||||
  Lists,
 | 
					  Lists,
 | 
				
			||||||
  Search,
 | 
					  Search,
 | 
				
			||||||
 | 
					  Directory,
 | 
				
			||||||
} from './util/async-components';
 | 
					} from './util/async-components';
 | 
				
			||||||
import { me, forceSingleColumn } from '../../initial_state';
 | 
					import { me, forceSingleColumn } from '../../initial_state';
 | 
				
			||||||
import { previewState as previewMediaState } from './components/media_modal';
 | 
					import { previewState as previewMediaState } from './components/media_modal';
 | 
				
			||||||
| 
						 | 
					@ -188,6 +189,7 @@ class SwitchingColumnsArea extends React.PureComponent {
 | 
				
			||||||
          <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
 | 
					          <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <WrappedRoute path='/search' component={Search} content={children} />
 | 
					          <WrappedRoute path='/search' component={Search} content={children} />
 | 
				
			||||||
 | 
					          <WrappedRoute path='/directory' component={Directory} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <WrappedRoute path='/statuses/new' component={Compose} content={children} />
 | 
					          <WrappedRoute path='/statuses/new' component={Compose} content={children} />
 | 
				
			||||||
          <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
 | 
					          <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} componentParams={{ shouldUpdateScroll: this.shouldUpdateScroll }} />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -141,3 +141,7 @@ export function Tesseract () {
 | 
				
			||||||
export function Audio () {
 | 
					export function Audio () {
 | 
				
			||||||
  return import(/* webpackChunkName: "features/audio" */'../../audio');
 | 
					  return import(/* webpackChunkName: "features/audio" */'../../audio');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function Directory () {
 | 
				
			||||||
 | 
					  return import(/* webpackChunkName: "features/directory" */'../../directory');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,14 @@ import {
 | 
				
			||||||
  MUTES_FETCH_SUCCESS,
 | 
					  MUTES_FETCH_SUCCESS,
 | 
				
			||||||
  MUTES_EXPAND_SUCCESS,
 | 
					  MUTES_EXPAND_SUCCESS,
 | 
				
			||||||
} from '../actions/mutes';
 | 
					} from '../actions/mutes';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  DIRECTORY_FETCH_REQUEST,
 | 
				
			||||||
 | 
					  DIRECTORY_FETCH_SUCCESS,
 | 
				
			||||||
 | 
					  DIRECTORY_FETCH_FAIL,
 | 
				
			||||||
 | 
					  DIRECTORY_EXPAND_REQUEST,
 | 
				
			||||||
 | 
					  DIRECTORY_EXPAND_SUCCESS,
 | 
				
			||||||
 | 
					  DIRECTORY_EXPAND_FAIL,
 | 
				
			||||||
 | 
					} from 'mastodon/actions/directory';
 | 
				
			||||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 | 
					import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const initialState = ImmutableMap({
 | 
					const initialState = ImmutableMap({
 | 
				
			||||||
| 
						 | 
					@ -74,6 +82,16 @@ export default function userLists(state = initialState, action) {
 | 
				
			||||||
    return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
 | 
					    return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
 | 
				
			||||||
  case MUTES_EXPAND_SUCCESS:
 | 
					  case MUTES_EXPAND_SUCCESS:
 | 
				
			||||||
    return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
 | 
					    return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
 | 
				
			||||||
 | 
					  case DIRECTORY_FETCH_SUCCESS:
 | 
				
			||||||
 | 
					    return state.setIn(['directory', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false);
 | 
				
			||||||
 | 
					  case DIRECTORY_EXPAND_SUCCESS:
 | 
				
			||||||
 | 
					    return state.updateIn(['directory', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false);
 | 
				
			||||||
 | 
					  case DIRECTORY_FETCH_REQUEST:
 | 
				
			||||||
 | 
					  case DIRECTORY_EXPAND_REQUEST:
 | 
				
			||||||
 | 
					    return state.setIn(['directory', 'isLoading'], true);
 | 
				
			||||||
 | 
					  case DIRECTORY_FETCH_FAIL:
 | 
				
			||||||
 | 
					  case DIRECTORY_EXPAND_FAIL:
 | 
				
			||||||
 | 
					    return state.setIn(['directory', 'isLoading'], false);
 | 
				
			||||||
  default:
 | 
					  default:
 | 
				
			||||||
    return state;
 | 
					    return state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2092,13 +2092,23 @@ a.account__display-name {
 | 
				
			||||||
    padding: 0;
 | 
					    padding: 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  //.column {
 | 
					  .directory__list {
 | 
				
			||||||
  //  margin-top: 0;
 | 
					    display: grid;
 | 
				
			||||||
 | 
					    grid-gap: 10px;
 | 
				
			||||||
 | 
					    grid-template-columns: minmax(0, 50%) minmax(0, 50%);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  //  @media screen and (min-width: $no-gap-breakpoint) {
 | 
					    @media screen and (max-width: $no-gap-breakpoint) {
 | 
				
			||||||
  //    margin-top: 10px;
 | 
					      display: block;
 | 
				
			||||||
  //  }
 | 
					    }
 | 
				
			||||||
  //}
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .directory__card {
 | 
				
			||||||
 | 
					    margin-bottom: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .filter-form {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .autosuggest-textarea__textarea {
 | 
					  .autosuggest-textarea__textarea {
 | 
				
			||||||
    font-size: 16px;
 | 
					    font-size: 16px;
 | 
				
			||||||
| 
						 | 
					@ -4982,59 +4992,6 @@ a.status-card.compact:hover {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/* End Media Gallery */
 | 
					/* End Media Gallery */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Status Video Player */
 | 
					 | 
				
			||||||
.status__video-player {
 | 
					 | 
				
			||||||
  background: $base-overlay-background;
 | 
					 | 
				
			||||||
  box-sizing: border-box;
 | 
					 | 
				
			||||||
  cursor: default; /* May not be needed */
 | 
					 | 
				
			||||||
  margin-top: 8px;
 | 
					 | 
				
			||||||
  overflow: hidden;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status__video-player-video {
 | 
					 | 
				
			||||||
  height: 100%;
 | 
					 | 
				
			||||||
  object-fit: cover;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  top: 50%;
 | 
					 | 
				
			||||||
  transform: translateY(-50%);
 | 
					 | 
				
			||||||
  width: 100%;
 | 
					 | 
				
			||||||
  z-index: 1;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status__video-player-expand,
 | 
					 | 
				
			||||||
.status__video-player-mute {
 | 
					 | 
				
			||||||
  color: $primary-text-color;
 | 
					 | 
				
			||||||
  opacity: 0.8;
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  right: 4px;
 | 
					 | 
				
			||||||
  text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status__video-player-spoiler {
 | 
					 | 
				
			||||||
  display: none;
 | 
					 | 
				
			||||||
  color: $primary-text-color;
 | 
					 | 
				
			||||||
  left: 4px;
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color;
 | 
					 | 
				
			||||||
  top: 4px;
 | 
					 | 
				
			||||||
  z-index: 100;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  &.status__video-player-spoiler--visible {
 | 
					 | 
				
			||||||
    display: block;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status__video-player-expand {
 | 
					 | 
				
			||||||
  bottom: 4px;
 | 
					 | 
				
			||||||
  z-index: 100;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.status__video-player-mute {
 | 
					 | 
				
			||||||
  top: 4px;
 | 
					 | 
				
			||||||
  z-index: 5;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.detailed,
 | 
					.detailed,
 | 
				
			||||||
.fullscreen {
 | 
					.fullscreen {
 | 
				
			||||||
  .video-player__volume__current,
 | 
					  .video-player__volume__current,
 | 
				
			||||||
| 
						 | 
					@ -5387,28 +5344,137 @@ a.status-card.compact:hover {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.media-spoiler-video {
 | 
					.directory {
 | 
				
			||||||
  background-size: cover;
 | 
					  &__list {
 | 
				
			||||||
  background-repeat: no-repeat;
 | 
					    width: 100%;
 | 
				
			||||||
  background-position: center;
 | 
					    margin: 10px 0;
 | 
				
			||||||
  cursor: pointer;
 | 
					    transition: opacity 100ms ease-in;
 | 
				
			||||||
  margin-top: 8px;
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  border: 0;
 | 
					 | 
				
			||||||
  display: block;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
.media-spoiler-video-play-icon {
 | 
					    &.loading {
 | 
				
			||||||
  border-radius: 100px;
 | 
					      opacity: 0.7;
 | 
				
			||||||
  color: rgba($primary-text-color, 0.8);
 | 
					    }
 | 
				
			||||||
  font-size: 36px;
 | 
					
 | 
				
			||||||
  left: 50%;
 | 
					    @media screen and (max-width: $no-gap-breakpoint) {
 | 
				
			||||||
  padding: 5px;
 | 
					      margin: 0;
 | 
				
			||||||
  position: absolute;
 | 
					    }
 | 
				
			||||||
  top: 50%;
 | 
					  }
 | 
				
			||||||
  transform: translate(-50%, -50%);
 | 
					
 | 
				
			||||||
 | 
					  &__card {
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__img {
 | 
				
			||||||
 | 
					      height: 125px;
 | 
				
			||||||
 | 
					      position: relative;
 | 
				
			||||||
 | 
					      background: darken($ui-base-color, 12%);
 | 
				
			||||||
 | 
					      overflow: hidden;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      img {
 | 
				
			||||||
 | 
					        display: block;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					        margin: 0;
 | 
				
			||||||
 | 
					        object-fit: cover;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__bar {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      background: lighten($ui-base-color, 4%);
 | 
				
			||||||
 | 
					      padding: 10px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &__name {
 | 
				
			||||||
 | 
					        flex: 1 1 auto;
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					        text-decoration: none;
 | 
				
			||||||
 | 
					        overflow: hidden;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &__relationship {
 | 
				
			||||||
 | 
					        width: 23px;
 | 
				
			||||||
 | 
					        min-height: 1px;
 | 
				
			||||||
 | 
					        flex: 0 0 auto;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .avatar {
 | 
				
			||||||
 | 
					        flex: 0 0 auto;
 | 
				
			||||||
 | 
					        width: 48px;
 | 
				
			||||||
 | 
					        height: 48px;
 | 
				
			||||||
 | 
					        padding-top: 2px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        img {
 | 
				
			||||||
 | 
					          width: 100%;
 | 
				
			||||||
 | 
					          height: 100%;
 | 
				
			||||||
 | 
					          display: block;
 | 
				
			||||||
 | 
					          margin: 0;
 | 
				
			||||||
 | 
					          border-radius: 4px;
 | 
				
			||||||
 | 
					          background: darken($ui-base-color, 8%);
 | 
				
			||||||
 | 
					          object-fit: cover;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .display-name {
 | 
				
			||||||
 | 
					        margin-left: 15px;
 | 
				
			||||||
 | 
					        text-align: left;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        strong {
 | 
				
			||||||
 | 
					          font-size: 15px;
 | 
				
			||||||
 | 
					          color: $primary-text-color;
 | 
				
			||||||
 | 
					          font-weight: 500;
 | 
				
			||||||
 | 
					          overflow: hidden;
 | 
				
			||||||
 | 
					          text-overflow: ellipsis;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        span {
 | 
				
			||||||
 | 
					          display: block;
 | 
				
			||||||
 | 
					          font-size: 14px;
 | 
				
			||||||
 | 
					          color: $darker-text-color;
 | 
				
			||||||
 | 
					          font-weight: 400;
 | 
				
			||||||
 | 
					          overflow: hidden;
 | 
				
			||||||
 | 
					          text-overflow: ellipsis;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__extra {
 | 
				
			||||||
 | 
					      background: $ui-base-color;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					      justify-content: center;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .accounts-table__count {
 | 
				
			||||||
 | 
					        width: 33.33%;
 | 
				
			||||||
 | 
					        flex: 0 0 auto;
 | 
				
			||||||
 | 
					        padding: 15px 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .account__header__content {
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        padding: 15px 10px;
 | 
				
			||||||
 | 
					        border-bottom: 1px solid lighten($ui-base-color, 8%);
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        min-height: 18px + 30px;
 | 
				
			||||||
 | 
					        white-space: nowrap;
 | 
				
			||||||
 | 
					        overflow: hidden;
 | 
				
			||||||
 | 
					        text-overflow: ellipsis;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        p {
 | 
				
			||||||
 | 
					          display: none;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          &:first-child {
 | 
				
			||||||
 | 
					            display: inline;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        br {
 | 
				
			||||||
 | 
					          display: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/* End Video Player */
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
.account-gallery__container {
 | 
					.account-gallery__container {
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
| 
						 | 
					@ -5484,6 +5550,73 @@ a.status-card.compact:hover {
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &.directory__section-headline {
 | 
				
			||||||
 | 
					    background: darken($ui-base-color, 2%);
 | 
				
			||||||
 | 
					    border-bottom-color: transparent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    a,
 | 
				
			||||||
 | 
					    button {
 | 
				
			||||||
 | 
					      &.active {
 | 
				
			||||||
 | 
					        &::before {
 | 
				
			||||||
 | 
					          display: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &::after {
 | 
				
			||||||
 | 
					          border-color: transparent transparent darken($ui-base-color, 7%);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.filter-form {
 | 
				
			||||||
 | 
					  background: $ui-base-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &__column {
 | 
				
			||||||
 | 
					    padding: 10px 15px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .radio-button {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.radio-button {
 | 
				
			||||||
 | 
					  font-size: 14px;
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					  padding: 6px 0;
 | 
				
			||||||
 | 
					  line-height: 18px;
 | 
				
			||||||
 | 
					  cursor: default;
 | 
				
			||||||
 | 
					  white-space: nowrap;
 | 
				
			||||||
 | 
					  overflow: hidden;
 | 
				
			||||||
 | 
					  text-overflow: ellipsis;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  input[type=radio],
 | 
				
			||||||
 | 
					  input[type=checkbox] {
 | 
				
			||||||
 | 
					    display: none;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &__input {
 | 
				
			||||||
 | 
					    display: inline-block;
 | 
				
			||||||
 | 
					    position: relative;
 | 
				
			||||||
 | 
					    border: 1px solid $ui-primary-color;
 | 
				
			||||||
 | 
					    box-sizing: border-box;
 | 
				
			||||||
 | 
					    width: 18px;
 | 
				
			||||||
 | 
					    height: 18px;
 | 
				
			||||||
 | 
					    flex: 0 0 auto;
 | 
				
			||||||
 | 
					    margin-right: 10px;
 | 
				
			||||||
 | 
					    top: -1px;
 | 
				
			||||||
 | 
					    border-radius: 50%;
 | 
				
			||||||
 | 
					    vertical-align: middle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &.checked {
 | 
				
			||||||
 | 
					      border-color: lighten($ui-highlight-color, 8%);
 | 
				
			||||||
 | 
					      background: lighten($ui-highlight-color, 8%);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
::-webkit-scrollbar-thumb {
 | 
					::-webkit-scrollbar-thumb {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -763,6 +763,24 @@
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .directory__list {
 | 
				
			||||||
 | 
					    display: grid;
 | 
				
			||||||
 | 
					    grid-gap: 10px;
 | 
				
			||||||
 | 
					    grid-template-columns: minmax(0, 50%) minmax(0, 50%);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @media screen and (max-width: $no-gap-breakpoint) {
 | 
				
			||||||
 | 
					      display: block;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .icon-button {
 | 
				
			||||||
 | 
					      font-size: 18px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .directory__card {
 | 
				
			||||||
 | 
					    margin-bottom: 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .card-grid {
 | 
					  .card-grid {
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    flex-wrap: wrap;
 | 
					    flex-wrap: wrap;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -20,6 +20,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
 | 
				
			||||||
    focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
 | 
					    focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
 | 
				
			||||||
    identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
 | 
					    identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
 | 
				
			||||||
    blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
 | 
					    blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
 | 
				
			||||||
 | 
					    discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
 | 
				
			||||||
  }.freeze
 | 
					  }.freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def self.default_key_transform
 | 
					  def self.default_key_transform
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,7 +51,6 @@
 | 
				
			||||||
class Account < ApplicationRecord
 | 
					class Account < ApplicationRecord
 | 
				
			||||||
  USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
 | 
					  USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
 | 
				
			||||||
  MENTION_RE  = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
 | 
					  MENTION_RE  = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
 | 
				
			||||||
  MIN_FOLLOWERS_DISCOVERY = 10
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  include AccountAssociations
 | 
					  include AccountAssociations
 | 
				
			||||||
  include AccountAvatar
 | 
					  include AccountAvatar
 | 
				
			||||||
| 
						 | 
					@ -104,11 +103,13 @@ class Account < ApplicationRecord
 | 
				
			||||||
  scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
 | 
					  scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
 | 
				
			||||||
  scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
 | 
					  scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
 | 
				
			||||||
  scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
 | 
					  scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
 | 
				
			||||||
  scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)) }
 | 
					  scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
 | 
				
			||||||
  scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
 | 
					  scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
 | 
				
			||||||
  scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) }
 | 
					  scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
 | 
				
			||||||
  scope :popular, -> { order('account_stats.followers_count desc') }
 | 
					  scope :popular, -> { order('account_stats.followers_count desc') }
 | 
				
			||||||
  scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
 | 
					  scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
 | 
				
			||||||
 | 
					  scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
 | 
				
			||||||
 | 
					  scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  delegate :email,
 | 
					  delegate :email,
 | 
				
			||||||
           :unconfirmed_email,
 | 
					           :unconfirmed_email,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,11 @@ class Feed
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def get(limit, max_id = nil, since_id = nil, min_id = nil)
 | 
					  def get(limit, max_id = nil, since_id = nil, min_id = nil)
 | 
				
			||||||
 | 
					    limit    = limit.to_i
 | 
				
			||||||
 | 
					    max_id   = max_id.to_i if max_id.present?
 | 
				
			||||||
 | 
					    since_id = since_id.to_i if since_id.present?
 | 
				
			||||||
 | 
					    min_id   = min_id.to_i if min_id.present?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from_redis(limit, max_id, since_id, min_id)
 | 
					    from_redis(limit, max_id, since_id, min_id)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,12 +28,12 @@ class MediaAttachment < ApplicationRecord
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  IMAGE_FILE_EXTENSIONS = %w(.jpg .jpeg .png .gif).freeze
 | 
					  IMAGE_FILE_EXTENSIONS = %w(.jpg .jpeg .png .gif).freeze
 | 
				
			||||||
  VIDEO_FILE_EXTENSIONS = %w(.webm .mp4 .m4v .mov).freeze
 | 
					  VIDEO_FILE_EXTENSIONS = %w(.webm .mp4 .m4v .mov).freeze
 | 
				
			||||||
  AUDIO_FILE_EXTENSIONS = %w(.ogg .oga .mp3 .wav .flac .opus .aac .m4a .3gp).freeze
 | 
					  AUDIO_FILE_EXTENSIONS = %w(.ogg .oga .mp3 .wav .flac .opus .aac .m4a .3gp .wma).freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  IMAGE_MIME_TYPES             = %w(image/jpeg image/png image/gif).freeze
 | 
					  IMAGE_MIME_TYPES             = %w(image/jpeg image/png image/gif).freeze
 | 
				
			||||||
  VIDEO_MIME_TYPES             = %w(video/webm video/mp4 video/quicktime video/ogg).freeze
 | 
					  VIDEO_MIME_TYPES             = %w(video/webm video/mp4 video/quicktime video/ogg).freeze
 | 
				
			||||||
  VIDEO_CONVERTIBLE_MIME_TYPES = %w(video/webm video/quicktime).freeze
 | 
					  VIDEO_CONVERTIBLE_MIME_TYPES = %w(video/webm video/quicktime).freeze
 | 
				
			||||||
  AUDIO_MIME_TYPES             = %w(audio/wave audio/wav audio/x-wav audio/x-pn-wave audio/ogg audio/mpeg audio/mp3 audio/webm audio/flac audio/aac audio/m4a audio/3gpp).freeze
 | 
					  AUDIO_MIME_TYPES             = %w(audio/wave audio/wav audio/x-wav audio/x-pn-wave audio/ogg audio/mpeg audio/mp3 audio/webm audio/flac audio/aac audio/m4a audio/x-m4a audio/mp4 audio/3gpp video/x-ms-asf).freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  BLURHASH_OPTIONS = {
 | 
					  BLURHASH_OPTIONS = {
 | 
				
			||||||
    x_comp: 4,
 | 
					    x_comp: 4,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,7 +6,7 @@ class RemoteFollow
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  attr_accessor :acct, :addressable_template
 | 
					  attr_accessor :acct, :addressable_template
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  validates :acct, presence: true
 | 
					  validates :acct, presence: true, domain: { acct: true }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def initialize(attrs = {})
 | 
					  def initialize(attrs = {})
 | 
				
			||||||
    @acct = normalize_acct(attrs[:acct])
 | 
					    @acct = normalize_acct(attrs[:acct])
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ class RemoteFollow
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def subscribe_address_for(account)
 | 
					  def subscribe_address_for(account)
 | 
				
			||||||
    addressable_template.expand(uri: account.local_username_and_domain).to_s
 | 
					    addressable_template.expand(uri: ActivityPub::TagManager.instance.uri_for(account)).to_s
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def interact_address_for(status)
 | 
					  def interact_address_for(status)
 | 
				
			||||||
| 
						 | 
					@ -44,6 +44,8 @@ class RemoteFollow
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [username, domain].compact.join('@')
 | 
					    [username, domain].compact.join('@')
 | 
				
			||||||
 | 
					  rescue Addressable::URI::InvalidURIError
 | 
				
			||||||
 | 
					    value
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def fetch_template!
 | 
					  def fetch_template!
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,12 +6,14 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
 | 
				
			||||||
  context :security
 | 
					  context :security
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  context_extensions :manually_approves_followers, :featured, :also_known_as,
 | 
					  context_extensions :manually_approves_followers, :featured, :also_known_as,
 | 
				
			||||||
                     :moved_to, :property_value, :hashtag, :emoji, :identity_proof
 | 
					                     :moved_to, :property_value, :hashtag, :emoji, :identity_proof,
 | 
				
			||||||
 | 
					                     :discoverable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  attributes :id, :type, :following, :followers,
 | 
					  attributes :id, :type, :following, :followers,
 | 
				
			||||||
             :inbox, :outbox, :featured,
 | 
					             :inbox, :outbox, :featured,
 | 
				
			||||||
             :preferred_username, :name, :summary,
 | 
					             :preferred_username, :name, :summary,
 | 
				
			||||||
             :url, :manually_approves_followers
 | 
					             :url, :manually_approves_followers,
 | 
				
			||||||
 | 
					             :discoverable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has_one :public_key, serializer: ActivityPub::PublicKeySerializer
 | 
					  has_one :public_key, serializer: ActivityPub::PublicKeySerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at,
 | 
					  attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at,
 | 
				
			||||||
             :note, :url, :avatar, :avatar_static, :header, :header_static,
 | 
					             :note, :url, :avatar, :avatar_static, :header, :header_static,
 | 
				
			||||||
             :followers_count, :following_count, :statuses_count
 | 
					             :followers_count, :following_count, :statuses_count, :last_status_at
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
 | 
					  has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
 | 
				
			||||||
  has_many :emojis, serializer: REST::CustomEmojiSerializer
 | 
					  has_many :emojis, serializer: REST::CustomEmojiSerializer
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -83,6 +83,7 @@ class ActivityPub::ProcessAccountService < BaseService
 | 
				
			||||||
    @account.fields                  = property_values || {}
 | 
					    @account.fields                  = property_values || {}
 | 
				
			||||||
    @account.also_known_as           = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) }
 | 
					    @account.also_known_as           = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) }
 | 
				
			||||||
    @account.actor_type              = actor_type
 | 
					    @account.actor_type              = actor_type
 | 
				
			||||||
 | 
					    @account.discoverable            = @json['discoverable'] || false
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def set_fetchable_attributes!
 | 
					  def set_fetchable_attributes!
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,7 +49,13 @@ class PostStatusService < BaseService
 | 
				
			||||||
  def preprocess_attributes!
 | 
					  def preprocess_attributes!
 | 
				
			||||||
    if @text.blank? && @options[:spoiler_text].present?
 | 
					    if @text.blank? && @options[:spoiler_text].present?
 | 
				
			||||||
     @text = '.'
 | 
					     @text = '.'
 | 
				
			||||||
     @text = @media.find(&:video?) ? '📹' : '🖼' if @media.size > 0
 | 
					     if @media.find(&:video?) || @media.find(&:gifv?)
 | 
				
			||||||
 | 
					       @text = '📹'
 | 
				
			||||||
 | 
					     elsif @media.find(&:audio?)
 | 
				
			||||||
 | 
					       @text = '🎵'
 | 
				
			||||||
 | 
					     elsif @media.find(&:image?)
 | 
				
			||||||
 | 
					       @text = '🖼'
 | 
				
			||||||
 | 
					     end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
    @visibility   = @options[:visibility] || @account.user&.setting_default_privacy
 | 
					    @visibility   = @options[:visibility] || @account.user&.setting_default_privacy
 | 
				
			||||||
    @visibility   = :unlisted if @visibility == :public && @account.silenced?
 | 
					    @visibility   = :unlisted if @visibility == :public && @account.silenced?
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,14 +4,22 @@ class DomainValidator < ActiveModel::EachValidator
 | 
				
			||||||
  def validate_each(record, attribute, value)
 | 
					  def validate_each(record, attribute, value)
 | 
				
			||||||
    return if value.blank?
 | 
					    return if value.blank?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    record.errors.add(attribute, I18n.t('domain_validator.invalid_domain')) unless compliant?(value)
 | 
					    domain = begin
 | 
				
			||||||
 | 
					      if options[:acct]
 | 
				
			||||||
 | 
					        value.split('@').last
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        value
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    record.errors.add(attribute, I18n.t('domain_validator.invalid_domain')) unless compliant?(domain)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private
 | 
					  private
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def compliant?(value)
 | 
					  def compliant?(value)
 | 
				
			||||||
    Addressable::URI.new.tap { |uri| uri.host = value }
 | 
					    Addressable::URI.new.tap { |uri| uri.host = value }
 | 
				
			||||||
  rescue Addressable::URI::InvalidURIError
 | 
					  rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
 | 
				
			||||||
    false
 | 
					    false
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,7 @@ class EmailMxValidator < ActiveModel::Validator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true if domain.nil?
 | 
					    return true if domain.nil?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    domain    = TagManager.instance.normalize_domain(domain)
 | 
				
			||||||
    hostnames = []
 | 
					    hostnames = []
 | 
				
			||||||
    ips       = []
 | 
					    ips       = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +30,8 @@ class EmailMxValidator < ActiveModel::Validator
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ips.empty? || on_blacklist?(hostnames + ips)
 | 
					    ips.empty? || on_blacklist?(hostnames + ips)
 | 
				
			||||||
 | 
					  rescue Addressable::URI::InvalidURIError
 | 
				
			||||||
 | 
					    true
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def on_blacklist?(values)
 | 
					  def on_blacklist?(values)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@
 | 
				
			||||||
        = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo'
 | 
					        = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      .display-name
 | 
					      .display-name
 | 
				
			||||||
        %span{id: "default_account_display_name", style: "display:none;"}= account.username
 | 
					        %span{ id: "default_account_display_name", style: "display: none" }= account.username
 | 
				
			||||||
        %bdi
 | 
					        %bdi
 | 
				
			||||||
          %strong.emojify.p-name= display_name(account, custom_emojify: true)
 | 
					          %strong.emojify.p-name= display_name(account, custom_emojify: true)
 | 
				
			||||||
        %span
 | 
					        %span
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,58 +14,43 @@
 | 
				
			||||||
  %h1= t('directories.explore_mastodon', title: site_title)
 | 
					  %h1= t('directories.explore_mastodon', title: site_title)
 | 
				
			||||||
  %p= t('directories.explanation')
 | 
					  %p= t('directories.explanation')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.grid
 | 
					- if @accounts.empty?
 | 
				
			||||||
  .column-0
 | 
					  = nothing_here
 | 
				
			||||||
    - if @accounts.empty?
 | 
					- else
 | 
				
			||||||
      = nothing_here
 | 
					  .directory__list
 | 
				
			||||||
    - else
 | 
					    - @accounts.each do |account|
 | 
				
			||||||
      .directory
 | 
					      .directory__card
 | 
				
			||||||
        %table.accounts-table
 | 
					        .directory__card__img
 | 
				
			||||||
          %tbody
 | 
					          = image_tag account.header.url, alt: ''
 | 
				
			||||||
            - @accounts.each do |account|
 | 
					        .directory__card__bar
 | 
				
			||||||
              %tr
 | 
					          = link_to TagManager.instance.url_for(account), class: 'directory__card__bar__name' do
 | 
				
			||||||
                %td= account_link_to account
 | 
					            .avatar
 | 
				
			||||||
                %td.accounts-table__count.optional
 | 
					              = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo'
 | 
				
			||||||
                  = number_to_human account.statuses_count, strip_insignificant_zeros: true
 | 
					 | 
				
			||||||
                  %small= t('accounts.posts', count: account.statuses_count).downcase
 | 
					 | 
				
			||||||
                %td.accounts-table__count.optional
 | 
					 | 
				
			||||||
                  = hide_followers_count?(account) ? '-' : (number_to_human account.followers_count, strip_insignificant_zeros: true)
 | 
					 | 
				
			||||||
                  %small= t('accounts.followers', count: account.followers_count).downcase
 | 
					 | 
				
			||||||
                %td.accounts-table__count
 | 
					 | 
				
			||||||
                  - if account.last_status_at.present?
 | 
					 | 
				
			||||||
                    %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
 | 
					 | 
				
			||||||
                  - else
 | 
					 | 
				
			||||||
                    \-
 | 
					 | 
				
			||||||
                  %small= t('accounts.last_active')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      = paginate @accounts
 | 
					            .display-name
 | 
				
			||||||
 | 
					              %span{ id: "default_account_display_name", style: "display: none" }= account.username
 | 
				
			||||||
 | 
					              %bdi
 | 
				
			||||||
 | 
					                %strong.emojify.p-name= display_name(account, custom_emojify: true)
 | 
				
			||||||
 | 
					              %span= acct(account)
 | 
				
			||||||
 | 
					          .directory__card__bar__relationship.account__relationship
 | 
				
			||||||
 | 
					            = minimal_account_action_button(account)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .column-1
 | 
					        .directory__card__extra
 | 
				
			||||||
    - if user_signed_in?
 | 
					          .account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true)
 | 
				
			||||||
      .box-widget.notice-widget
 | 
					 | 
				
			||||||
        - if current_account.discoverable?
 | 
					 | 
				
			||||||
          - if current_account.followers_count < Account::MIN_FOLLOWERS_DISCOVERY
 | 
					 | 
				
			||||||
            %p= t('directories.enabled_but_waiting', min_followers: Account::MIN_FOLLOWERS_DISCOVERY)
 | 
					 | 
				
			||||||
          - else
 | 
					 | 
				
			||||||
            %p= t('directories.enabled')
 | 
					 | 
				
			||||||
        - else
 | 
					 | 
				
			||||||
          %p= t('directories.how_to_enable')
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          = link_to settings_profile_path do
 | 
					        .directory__card__extra
 | 
				
			||||||
            = t('settings.edit_profile')
 | 
					          .accounts-table__count
 | 
				
			||||||
            = fa_icon 'chevron-right fw'
 | 
					            = number_to_human account.statuses_count, strip_insignificant_zeros: true
 | 
				
			||||||
 | 
					            %small= t('accounts.posts', count: account.statuses_count).downcase
 | 
				
			||||||
 | 
					          .accounts-table__count
 | 
				
			||||||
 | 
					            = hide_followers_count?(account) ? '-' : (number_to_human account.followers_count, strip_insignificant_zeros: true)
 | 
				
			||||||
 | 
					            %small= t('accounts.followers', count: account.followers_count).downcase
 | 
				
			||||||
 | 
					          .accounts-table__count
 | 
				
			||||||
 | 
					            - if account.last_status_at.present?
 | 
				
			||||||
 | 
					              %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
 | 
				
			||||||
 | 
					            - else
 | 
				
			||||||
 | 
					              = t('invites.expires_in_prompt')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    - if @tags.empty? && !user_signed_in?
 | 
					            %small= t('accounts.last_active')
 | 
				
			||||||
      .nothing-here
 | 
					 | 
				
			||||||
    - else
 | 
					 | 
				
			||||||
      - @tags.each do |tag|
 | 
					 | 
				
			||||||
        .directory__tag{ class: tag.id == @tag&.id ? 'active' : nil }
 | 
					 | 
				
			||||||
          = link_to explore_hashtag_path(tag) do
 | 
					 | 
				
			||||||
            %h4
 | 
					 | 
				
			||||||
              = fa_icon 'hashtag'
 | 
					 | 
				
			||||||
              = tag.name
 | 
					 | 
				
			||||||
              %small= t('directories.people', count: tag.accounts_count)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            .avatar-stack
 | 
					  = paginate @accounts
 | 
				
			||||||
              - tag.cached_sample_accounts.each do |account|
 | 
					 | 
				
			||||||
                = image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar'
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										5
									
								
								app/views/errors/400.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/views/errors/400.html.haml
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					- content_for :page_title do
 | 
				
			||||||
 | 
					  = t('errors.400')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- content_for :content do
 | 
				
			||||||
 | 
					  = t('errors.400')
 | 
				
			||||||
							
								
								
									
										5
									
								
								app/views/errors/406.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/views/errors/406.html.haml
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					- content_for :page_title do
 | 
				
			||||||
 | 
					  = t('errors.406')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- content_for :content do
 | 
				
			||||||
 | 
					  = t('errors.406')
 | 
				
			||||||
							
								
								
									
										5
									
								
								app/views/errors/503.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/views/errors/503.html.haml
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					- content_for :page_title do
 | 
				
			||||||
 | 
					  = t('errors.503')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- content_for :content do
 | 
				
			||||||
 | 
					  = t('errors.503')
 | 
				
			||||||
| 
						 | 
					@ -28,7 +28,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  - if Setting.profile_directory
 | 
					  - if Setting.profile_directory
 | 
				
			||||||
    .fields-group
 | 
					    .fields-group
 | 
				
			||||||
      = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable_html', min_followers: Account::MIN_FOLLOWERS_DISCOVERY, path: explore_path), recommended: true
 | 
					      = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable'), recommended: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  %hr.spacer/
 | 
					  %hr.spacer/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,11 +42,11 @@
 | 
				
			||||||
                              - unless @warning.text.blank?
 | 
					                              - unless @warning.text.blank?
 | 
				
			||||||
                                = Formatter.instance.linkify(@warning.text)
 | 
					                                = Formatter.instance.linkify(@warning.text)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                              - unless @statuses.empty?
 | 
					                              - unless @statuses&.empty?
 | 
				
			||||||
                                %p
 | 
					                                %p
 | 
				
			||||||
                                  %strong= t('user_mailer.warning.statuses')
 | 
					                                  %strong= t('user_mailer.warning.statuses')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- unless @statuses.empty?
 | 
					- unless @statuses&.empty?
 | 
				
			||||||
  - @statuses.each_with_index do |status, i|
 | 
					  - @statuses.each_with_index do |status, i|
 | 
				
			||||||
    = render 'notification_mailer/status', status: status, i: i + 1, highlighted: true
 | 
					    = render 'notification_mailer/status', status: status, i: i + 1, highlighted: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<% end %>
 | 
					<% end %>
 | 
				
			||||||
<%= @warning.text %>
 | 
					<%= @warning.text %>
 | 
				
			||||||
<% unless @statuses.empty? %>
 | 
					<% unless @statuses&.empty? %>
 | 
				
			||||||
<%= t('user_mailer.warning.statuses') %>
 | 
					<%= t('user_mailer.warning.statuses') %>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<% @statuses.each do |status| %>
 | 
					<% @statuses.each do |status| %>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -643,14 +643,8 @@ en:
 | 
				
			||||||
    warning_title: Disseminated content availability
 | 
					    warning_title: Disseminated content availability
 | 
				
			||||||
  directories:
 | 
					  directories:
 | 
				
			||||||
    directory: Profile directory
 | 
					    directory: Profile directory
 | 
				
			||||||
    enabled: You are currently listed in the directory.
 | 
					 | 
				
			||||||
    enabled_but_waiting: You have opted-in to be listed in the directory, but you do not have the minimum number of followers (%{min_followers}) to be listed yet.
 | 
					 | 
				
			||||||
    explanation: Discover users based on their interests
 | 
					    explanation: Discover users based on their interests
 | 
				
			||||||
    explore_mastodon: Explore %{title}
 | 
					    explore_mastodon: Explore %{title}
 | 
				
			||||||
    how_to_enable: You are not currently opted-in to the directory. You can opt-in below. Use hashtags in your bio text to be listed under specific hashtags!
 | 
					 | 
				
			||||||
    people:
 | 
					 | 
				
			||||||
      one: "%{count} person"
 | 
					 | 
				
			||||||
      other: "%{count} people"
 | 
					 | 
				
			||||||
  domain_blocks:
 | 
					  domain_blocks:
 | 
				
			||||||
    blocked_domains: List of limited and blocked domains
 | 
					    blocked_domains: List of limited and blocked domains
 | 
				
			||||||
    description: This is the list of servers that %{instance} limits or reject federation with.
 | 
					    description: This is the list of servers that %{instance} limits or reject federation with.
 | 
				
			||||||
| 
						 | 
					@ -671,8 +665,10 @@ en:
 | 
				
			||||||
  domain_validator:
 | 
					  domain_validator:
 | 
				
			||||||
    invalid_domain: is not a valid domain name
 | 
					    invalid_domain: is not a valid domain name
 | 
				
			||||||
  errors:
 | 
					  errors:
 | 
				
			||||||
 | 
					    '400': The request you submitted was invalid or malformed.
 | 
				
			||||||
    '403': You don't have permission to view this page.
 | 
					    '403': You don't have permission to view this page.
 | 
				
			||||||
    '404': The page you are looking for isn't here.
 | 
					    '404': The page you are looking for isn't here.
 | 
				
			||||||
 | 
					    '406': This page is not available in the requested format.
 | 
				
			||||||
    '410': The page you were looking for doesn't exist here anymore.
 | 
					    '410': The page you were looking for doesn't exist here anymore.
 | 
				
			||||||
    '422':
 | 
					    '422':
 | 
				
			||||||
      content: Security verification failed. Are you blocking cookies?
 | 
					      content: Security verification failed. Are you blocking cookies?
 | 
				
			||||||
| 
						 | 
					@ -681,6 +677,7 @@ en:
 | 
				
			||||||
    '500':
 | 
					    '500':
 | 
				
			||||||
      content: We're sorry, but something went wrong on our end.
 | 
					      content: We're sorry, but something went wrong on our end.
 | 
				
			||||||
      title: This page is not correct
 | 
					      title: This page is not correct
 | 
				
			||||||
 | 
					    '503': The page could not be served due to a temporary server failure.
 | 
				
			||||||
    noscript_html: To use the Mastodon web application, please enable JavaScript. Alternatively, try one of the <a href="%{apps_path}">native apps</a> for Mastodon for your platform.
 | 
					    noscript_html: To use the Mastodon web application, please enable JavaScript. Alternatively, try one of the <a href="%{apps_path}">native apps</a> for Mastodon for your platform.
 | 
				
			||||||
  existing_username_validator:
 | 
					  existing_username_validator:
 | 
				
			||||||
    not_found: could not find a local user with that username
 | 
					    not_found: could not find a local user with that username
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ en:
 | 
				
			||||||
        bot: This account mainly performs automated actions and might not be monitored
 | 
					        bot: This account mainly performs automated actions and might not be monitored
 | 
				
			||||||
        context: One or multiple contexts where the filter should apply
 | 
					        context: One or multiple contexts where the filter should apply
 | 
				
			||||||
        digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence
 | 
					        digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence
 | 
				
			||||||
        discoverable_html: The <a href="%{path}" target="_blank">directory</a> lets people find accounts based on interests and activity. Requires at least %{min_followers} followers
 | 
					        discoverable: The profile directory is another way by which your account can reach a wider audience
 | 
				
			||||||
        email: You will be sent a confirmation e-mail
 | 
					        email: You will be sent a confirmation e-mail
 | 
				
			||||||
        fields: You can have up to 4 items displayed as a table on your profile
 | 
					        fields: You can have up to 4 items displayed as a table on your profile
 | 
				
			||||||
        header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
 | 
					        header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,8 @@ require 'sidekiq-scheduler/web'
 | 
				
			||||||
Sidekiq::Web.set :session_secret, Rails.application.secrets[:secret_key_base]
 | 
					Sidekiq::Web.set :session_secret, Rails.application.secrets[:secret_key_base]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Rails.application.routes.draw do
 | 
					Rails.application.routes.draw do
 | 
				
			||||||
 | 
					  root 'home#index'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mount LetterOpenerWeb::Engine, at: 'letter_opener' if Rails.env.development?
 | 
					  mount LetterOpenerWeb::Engine, at: 'letter_opener' if Rails.env.development?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  authenticate :user, lambda { |u| u.admin? } do
 | 
					  authenticate :user, lambda { |u| u.admin? } do
 | 
				
			||||||
| 
						 | 
					@ -336,6 +338,7 @@ Rails.application.routes.draw do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      resource :domain_blocks, only: [:show, :create, :destroy]
 | 
					      resource :domain_blocks, only: [:show, :create, :destroy]
 | 
				
			||||||
 | 
					      resource :directory, only: [:show]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      resources :follow_requests, only: [:index] do
 | 
					      resources :follow_requests, only: [:index] do
 | 
				
			||||||
        member do
 | 
					        member do
 | 
				
			||||||
| 
						 | 
					@ -440,10 +443,6 @@ Rails.application.routes.draw do
 | 
				
			||||||
  get '/about/blocks', to: 'about#blocks'
 | 
					  get '/about/blocks', to: 'about#blocks'
 | 
				
			||||||
  get '/terms',        to: 'about#terms'
 | 
					  get '/terms',        to: 'about#terms'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  root 'home#index'
 | 
					  match '/', via: [:post, :put, :patch, :delete], to: 'application#raise_not_found', format: false
 | 
				
			||||||
 | 
					  match '*unmatched_route', via: :all, to: 'application#raise_not_found', format: false
 | 
				
			||||||
  match '*unmatched_route',
 | 
					 | 
				
			||||||
        via: :all,
 | 
					 | 
				
			||||||
        to: 'application#raise_not_found',
 | 
					 | 
				
			||||||
        format: false
 | 
					 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										2
									
								
								dist/nginx.conf
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								dist/nginx.conf
									
									
									
									
										vendored
									
									
								
							| 
						 | 
					@ -19,7 +19,7 @@ server {
 | 
				
			||||||
  listen [::]:443 ssl http2;
 | 
					  listen [::]:443 ssl http2;
 | 
				
			||||||
  server_name example.com;
 | 
					  server_name example.com;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ssl_protocols TLSv1.2;
 | 
					  ssl_protocols TLSv1.2 TLSv1.3;
 | 
				
			||||||
  ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
 | 
					  ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
 | 
				
			||||||
  ssl_prefer_server_ciphers on;
 | 
					  ssl_prefer_server_ciphers on;
 | 
				
			||||||
  ssl_session_cache shared:SSL:10m;
 | 
					  ssl_session_cache shared:SSL:10m;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,7 +2,7 @@
 | 
				
			||||||
  "name": "mastodon",
 | 
					  "name": "mastodon",
 | 
				
			||||||
  "license": "AGPL-3.0-or-later",
 | 
					  "license": "AGPL-3.0-or-later",
 | 
				
			||||||
  "engines": {
 | 
					  "engines": {
 | 
				
			||||||
    "node": ">=8.12 <12"
 | 
					    "node": ">=8.12 <13"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "postversion": "git push --tags",
 | 
					    "postversion": "git push --tags",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -66,9 +66,7 @@ describe RemoteFollowController do
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        it 'redirects to the remote location' do
 | 
					        it 'redirects to the remote location' do
 | 
				
			||||||
          address = "http://example.com/follow_me?acct=test_user%40#{Rails.configuration.x.local_domain}"
 | 
					          expect(response).to redirect_to("http://example.com/follow_me?acct=https%3A%2F%2F#{Rails.configuration.x.local_domain}%2Fusers%2Ftest_user")
 | 
				
			||||||
 | 
					 | 
				
			||||||
          expect(response).to redirect_to(address)
 | 
					 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,7 +50,8 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      describe 'when form_two_factor_confirmation parameter is not provided' do
 | 
					      describe 'when form_two_factor_confirmation parameter is not provided' do
 | 
				
			||||||
        it 'raises ActionController::ParameterMissing' do
 | 
					        it 'raises ActionController::ParameterMissing' do
 | 
				
			||||||
          expect { post :create, params: {} }.to raise_error(ActionController::ParameterMissing)
 | 
					          post :create, params: {}
 | 
				
			||||||
 | 
					          expect(response).to have_http_status(400)
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -112,7 +112,8 @@ describe Settings::TwoFactorAuthenticationsController do
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      it 'raises ActionController::ParameterMissing if code is missing' do
 | 
					      it 'raises ActionController::ParameterMissing if code is missing' do
 | 
				
			||||||
        expect { post :destroy }.to raise_error(ActionController::ParameterMissing)
 | 
					        post :destroy
 | 
				
			||||||
 | 
					        expect(response).to have_http_status(400)
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,7 +61,7 @@ RSpec.describe RemoteFollow do
 | 
				
			||||||
    subject { remote_follow.subscribe_address_for(account) }
 | 
					    subject { remote_follow.subscribe_address_for(account) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it 'returns subscribe address' do
 | 
					    it 'returns subscribe address' do
 | 
				
			||||||
      is_expected.to eq 'https://quitter.no/main/ostatussub?profile=alice%40cb6e6126.ngrok.io'
 | 
					      is_expected.to eq 'https://quitter.no/main/ostatussub?profile=https%3A%2F%2Fcb6e6126.ngrok.io%2Fusers%2Falice'
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue