Merge pull request #1187 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
		
						commit
						0aee74d78a
					
				
					 58 changed files with 296 additions and 203 deletions
				
			
		
							
								
								
									
										4
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								Gemfile
									
									
									
									
									
								
							| 
						 | 
					@ -113,7 +113,7 @@ group :production, :test do
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
group :test do
 | 
					group :test do
 | 
				
			||||||
  gem 'capybara', '~> 3.27'
 | 
					  gem 'capybara', '~> 3.28'
 | 
				
			||||||
  gem 'climate_control', '~> 0.2'
 | 
					  gem 'climate_control', '~> 0.2'
 | 
				
			||||||
  gem 'faker', '~> 1.9'
 | 
					  gem 'faker', '~> 1.9'
 | 
				
			||||||
  gem 'microformats', '~> 4.1'
 | 
					  gem 'microformats', '~> 4.1'
 | 
				
			||||||
| 
						 | 
					@ -133,7 +133,7 @@ group :development do
 | 
				
			||||||
  gem 'letter_opener', '~> 1.7'
 | 
					  gem 'letter_opener', '~> 1.7'
 | 
				
			||||||
  gem 'letter_opener_web', '~> 1.3'
 | 
					  gem 'letter_opener_web', '~> 1.3'
 | 
				
			||||||
  gem 'memory_profiler'
 | 
					  gem 'memory_profiler'
 | 
				
			||||||
  gem 'rubocop', '~> 0.73', require: false
 | 
					  gem 'rubocop', '~> 0.74', require: false
 | 
				
			||||||
  gem 'rubocop-rails', '~> 2.2', require: false
 | 
					  gem 'rubocop-rails', '~> 2.2', require: false
 | 
				
			||||||
  gem 'brakeman', '~> 4.6', require: false
 | 
					  gem 'brakeman', '~> 4.6', require: false
 | 
				
			||||||
  gem 'bundler-audit', '~> 0.6', require: false
 | 
					  gem 'bundler-audit', '~> 0.6', require: false
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										22
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								Gemfile.lock
									
									
									
									
									
								
							| 
						 | 
					@ -150,7 +150,7 @@ GEM
 | 
				
			||||||
      sshkit (~> 1.3)
 | 
					      sshkit (~> 1.3)
 | 
				
			||||||
    capistrano-yarn (2.0.2)
 | 
					    capistrano-yarn (2.0.2)
 | 
				
			||||||
      capistrano (~> 3.0)
 | 
					      capistrano (~> 3.0)
 | 
				
			||||||
    capybara (3.27.0)
 | 
					    capybara (3.28.0)
 | 
				
			||||||
      addressable
 | 
					      addressable
 | 
				
			||||||
      mini_mime (>= 0.1.3)
 | 
					      mini_mime (>= 0.1.3)
 | 
				
			||||||
      nokogiri (~> 1.8)
 | 
					      nokogiri (~> 1.8)
 | 
				
			||||||
| 
						 | 
					@ -209,9 +209,9 @@ GEM
 | 
				
			||||||
      unf (>= 0.0.5, < 1.0.0)
 | 
					      unf (>= 0.0.5, < 1.0.0)
 | 
				
			||||||
    doorkeeper (5.1.0)
 | 
					    doorkeeper (5.1.0)
 | 
				
			||||||
      railties (>= 5)
 | 
					      railties (>= 5)
 | 
				
			||||||
    dotenv (2.7.4)
 | 
					    dotenv (2.7.5)
 | 
				
			||||||
    dotenv-rails (2.7.4)
 | 
					    dotenv-rails (2.7.5)
 | 
				
			||||||
      dotenv (= 2.7.4)
 | 
					      dotenv (= 2.7.5)
 | 
				
			||||||
      railties (>= 3.2, < 6.1)
 | 
					      railties (>= 3.2, < 6.1)
 | 
				
			||||||
    elasticsearch (6.0.2)
 | 
					    elasticsearch (6.0.2)
 | 
				
			||||||
      elasticsearch-api (= 6.0.2)
 | 
					      elasticsearch-api (= 6.0.2)
 | 
				
			||||||
| 
						 | 
					@ -274,7 +274,7 @@ GEM
 | 
				
			||||||
      railties (>= 4.0.1)
 | 
					      railties (>= 4.0.1)
 | 
				
			||||||
    hamster (3.0.0)
 | 
					    hamster (3.0.0)
 | 
				
			||||||
      concurrent-ruby (~> 1.0)
 | 
					      concurrent-ruby (~> 1.0)
 | 
				
			||||||
    hashdiff (0.4.0)
 | 
					    hashdiff (1.0.0)
 | 
				
			||||||
    hashie (3.6.0)
 | 
					    hashie (3.6.0)
 | 
				
			||||||
    heapy (0.1.4)
 | 
					    heapy (0.1.4)
 | 
				
			||||||
    highline (2.0.1)
 | 
					    highline (2.0.1)
 | 
				
			||||||
| 
						 | 
					@ -478,7 +478,7 @@ GEM
 | 
				
			||||||
    rails-dom-testing (2.0.3)
 | 
					    rails-dom-testing (2.0.3)
 | 
				
			||||||
      activesupport (>= 4.2.0)
 | 
					      activesupport (>= 4.2.0)
 | 
				
			||||||
      nokogiri (>= 1.6)
 | 
					      nokogiri (>= 1.6)
 | 
				
			||||||
    rails-html-sanitizer (1.0.4)
 | 
					    rails-html-sanitizer (1.1.0)
 | 
				
			||||||
      loofah (~> 2.2, >= 2.2.2)
 | 
					      loofah (~> 2.2, >= 2.2.2)
 | 
				
			||||||
    rails-i18n (5.1.3)
 | 
					    rails-i18n (5.1.3)
 | 
				
			||||||
      i18n (>= 0.7, < 2)
 | 
					      i18n (>= 0.7, < 2)
 | 
				
			||||||
| 
						 | 
					@ -492,7 +492,7 @@ GEM
 | 
				
			||||||
      rake (>= 0.8.7)
 | 
					      rake (>= 0.8.7)
 | 
				
			||||||
      thor (>= 0.19.0, < 2.0)
 | 
					      thor (>= 0.19.0, < 2.0)
 | 
				
			||||||
    rainbow (3.0.0)
 | 
					    rainbow (3.0.0)
 | 
				
			||||||
    rake (12.3.2)
 | 
					    rake (12.3.3)
 | 
				
			||||||
    rdf (3.0.12)
 | 
					    rdf (3.0.12)
 | 
				
			||||||
      hamster (~> 3.0)
 | 
					      hamster (~> 3.0)
 | 
				
			||||||
      link_header (~> 0.0, >= 0.0.8)
 | 
					      link_header (~> 0.0, >= 0.0.8)
 | 
				
			||||||
| 
						 | 
					@ -548,7 +548,7 @@ GEM
 | 
				
			||||||
      rspec-core (~> 3.0, >= 3.0.0)
 | 
					      rspec-core (~> 3.0, >= 3.0.0)
 | 
				
			||||||
      sidekiq (>= 2.4.0)
 | 
					      sidekiq (>= 2.4.0)
 | 
				
			||||||
    rspec-support (3.8.0)
 | 
					    rspec-support (3.8.0)
 | 
				
			||||||
    rubocop (0.73.0)
 | 
					    rubocop (0.74.0)
 | 
				
			||||||
      jaro_winkler (~> 1.5.1)
 | 
					      jaro_winkler (~> 1.5.1)
 | 
				
			||||||
      parallel (~> 1.10)
 | 
					      parallel (~> 1.10)
 | 
				
			||||||
      parser (>= 2.6)
 | 
					      parser (>= 2.6)
 | 
				
			||||||
| 
						 | 
					@ -645,7 +645,7 @@ GEM
 | 
				
			||||||
    uniform_notifier (1.12.1)
 | 
					    uniform_notifier (1.12.1)
 | 
				
			||||||
    warden (1.2.8)
 | 
					    warden (1.2.8)
 | 
				
			||||||
      rack (>= 2.0.6)
 | 
					      rack (>= 2.0.6)
 | 
				
			||||||
    webmock (3.6.0)
 | 
					    webmock (3.6.2)
 | 
				
			||||||
      addressable (>= 2.3.6)
 | 
					      addressable (>= 2.3.6)
 | 
				
			||||||
      crack (>= 0.3.2)
 | 
					      crack (>= 0.3.2)
 | 
				
			||||||
      hashdiff (>= 0.4.0, < 2.0.0)
 | 
					      hashdiff (>= 0.4.0, < 2.0.0)
 | 
				
			||||||
| 
						 | 
					@ -684,7 +684,7 @@ DEPENDENCIES
 | 
				
			||||||
  capistrano-rails (~> 1.4)
 | 
					  capistrano-rails (~> 1.4)
 | 
				
			||||||
  capistrano-rbenv (~> 2.1)
 | 
					  capistrano-rbenv (~> 2.1)
 | 
				
			||||||
  capistrano-yarn (~> 2.0)
 | 
					  capistrano-yarn (~> 2.0)
 | 
				
			||||||
  capybara (~> 3.27)
 | 
					  capybara (~> 3.28)
 | 
				
			||||||
  charlock_holmes (~> 0.7.6)
 | 
					  charlock_holmes (~> 0.7.6)
 | 
				
			||||||
  chewy (~> 5.0)
 | 
					  chewy (~> 5.0)
 | 
				
			||||||
  cld3 (~> 3.2.4)
 | 
					  cld3 (~> 3.2.4)
 | 
				
			||||||
| 
						 | 
					@ -766,7 +766,7 @@ DEPENDENCIES
 | 
				
			||||||
  rqrcode (~> 0.10)
 | 
					  rqrcode (~> 0.10)
 | 
				
			||||||
  rspec-rails (~> 3.8)
 | 
					  rspec-rails (~> 3.8)
 | 
				
			||||||
  rspec-sidekiq (~> 3.0)
 | 
					  rspec-sidekiq (~> 3.0)
 | 
				
			||||||
  rubocop (~> 0.73)
 | 
					  rubocop (~> 0.74)
 | 
				
			||||||
  rubocop-rails (~> 2.2)
 | 
					  rubocop-rails (~> 2.2)
 | 
				
			||||||
  sanitize (~> 5.0)
 | 
					  sanitize (~> 5.0)
 | 
				
			||||||
  sidekiq (~> 5.2)
 | 
					  sidekiq (~> 5.2)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,17 +9,8 @@ module WellKnown
 | 
				
			||||||
    def show
 | 
					    def show
 | 
				
			||||||
      @account = Account.find_local!(username_from_resource)
 | 
					      @account = Account.find_local!(username_from_resource)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      respond_to do |format|
 | 
					 | 
				
			||||||
        format.any(:json, :html) do
 | 
					 | 
				
			||||||
          render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json'
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        format.xml do
 | 
					 | 
				
			||||||
          render content_type: 'application/xrd+xml'
 | 
					 | 
				
			||||||
        end
 | 
					 | 
				
			||||||
      end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expires_in 3.days, public: true
 | 
					      expires_in 3.days, public: true
 | 
				
			||||||
 | 
					      render json: @account, serializer: WebfingerSerializer, content_type: 'application/jrd+json'
 | 
				
			||||||
    rescue ActiveRecord::RecordNotFound
 | 
					    rescue ActiveRecord::RecordNotFound
 | 
				
			||||||
      head 404
 | 
					      head 404
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -182,6 +182,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
 | 
				
			||||||
          className='value'
 | 
					          className='value'
 | 
				
			||||||
          disabled={disabled}
 | 
					          disabled={disabled}
 | 
				
			||||||
          icon={icon}
 | 
					          icon={icon}
 | 
				
			||||||
 | 
					          inverted
 | 
				
			||||||
          onClick={handleToggle}
 | 
					          onClick={handleToggle}
 | 
				
			||||||
          size={18}
 | 
					          size={18}
 | 
				
			||||||
          style={{
 | 
					          style={{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ import spring from 'react-motion/lib/spring';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
//  Components.
 | 
					//  Components.
 | 
				
			||||||
import IconButton from 'flavours/glitch/components/icon_button';
 | 
					import IconButton from 'flavours/glitch/components/icon_button';
 | 
				
			||||||
import TextIconButton from 'flavours/glitch/components/text_icon_button';
 | 
					import TextIconButton from './text_icon_button';
 | 
				
			||||||
import Dropdown from './dropdown';
 | 
					import Dropdown from './dropdown';
 | 
				
			||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
					import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,12 @@
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const iconStyle = {
 | 
				
			||||||
 | 
					  height: null,
 | 
				
			||||||
 | 
					  lineHeight: '27px',
 | 
				
			||||||
 | 
					  width: `${18 * 1.28571429}px`,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class TextIconButton extends React.PureComponent {
 | 
					export default class TextIconButton extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static propTypes = {
 | 
					  static propTypes = {
 | 
				
			||||||
| 
						 | 
					@ -20,7 +26,15 @@ export default class TextIconButton extends React.PureComponent {
 | 
				
			||||||
    const { label, title, active, ariaControls } = this.props;
 | 
					    const { label, title, active, ariaControls } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}>
 | 
					      <button
 | 
				
			||||||
 | 
					        title={title}
 | 
				
			||||||
 | 
					        aria-label={title}
 | 
				
			||||||
 | 
					        className={`text-icon-button ${active ? 'active' : ''}`}
 | 
				
			||||||
 | 
					        aria-expanded={active}
 | 
				
			||||||
 | 
					        onClick={this.handleClick}
 | 
				
			||||||
 | 
					        aria-controls={ariaControls}
 | 
				
			||||||
 | 
					        style={iconStyle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        {label}
 | 
					        {label}
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@ const messages = defineMessages({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
  value: state.getIn(['listEditor', 'title']),
 | 
					  value: state.getIn(['listEditor', 'title']),
 | 
				
			||||||
  disabled: !state.getIn(['listEditor', 'isChanged']),
 | 
					  disabled: !state.getIn(['listEditor', 'isChanged']) || !state.getIn(['listEditor', 'title']),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = dispatch => ({
 | 
					const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -66,7 +66,7 @@ export default class NewListForm extends React.PureComponent {
 | 
				
			||||||
        </label>
 | 
					        </label>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <IconButton
 | 
					        <IconButton
 | 
				
			||||||
          disabled={disabled}
 | 
					          disabled={disabled || !value}
 | 
				
			||||||
          icon='plus'
 | 
					          icon='plus'
 | 
				
			||||||
          title={title}
 | 
					          title={title}
 | 
				
			||||||
          onClick={this.handleClick}
 | 
					          onClick={this.handleClick}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -118,20 +118,29 @@
 | 
				
			||||||
  display: inline-block;
 | 
					  display: inline-block;
 | 
				
			||||||
  padding: 0;
 | 
					  padding: 0;
 | 
				
			||||||
  color: $action-button-color;
 | 
					  color: $action-button-color;
 | 
				
			||||||
  border: none;
 | 
					  border: 0;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
  background: transparent;
 | 
					  background: transparent;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  transition: color 100ms ease-in;
 | 
					  transition: all 100ms ease-in;
 | 
				
			||||||
 | 
					  transition-property: background-color, color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover,
 | 
					  &:hover,
 | 
				
			||||||
  &:active,
 | 
					  &:active,
 | 
				
			||||||
  &:focus {
 | 
					  &:focus {
 | 
				
			||||||
    color: lighten($action-button-color, 7%);
 | 
					    color: lighten($action-button-color, 7%);
 | 
				
			||||||
    transition: color 200ms ease-out;
 | 
					    background-color: rgba($action-button-color, 0.15);
 | 
				
			||||||
 | 
					    transition: all 200ms ease-out;
 | 
				
			||||||
 | 
					    transition-property: background-color, color;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:focus {
 | 
				
			||||||
 | 
					    background-color: rgba($action-button-color, 0.3);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.disabled {
 | 
					  &.disabled {
 | 
				
			||||||
    color: darken($action-button-color, 13%);
 | 
					    color: darken($action-button-color, 13%);
 | 
				
			||||||
 | 
					    background-color: transparent;
 | 
				
			||||||
    cursor: default;
 | 
					    cursor: default;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -156,10 +165,16 @@
 | 
				
			||||||
    &:active,
 | 
					    &:active,
 | 
				
			||||||
    &:focus {
 | 
					    &:focus {
 | 
				
			||||||
      color: darken($lighter-text-color, 7%);
 | 
					      color: darken($lighter-text-color, 7%);
 | 
				
			||||||
 | 
					      background-color: rgba($lighter-text-color, 0.15);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:focus {
 | 
				
			||||||
 | 
					      background-color: rgba($lighter-text-color, 0.3);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &.disabled {
 | 
					    &.disabled {
 | 
				
			||||||
      color: lighten($lighter-text-color, 7%);
 | 
					      color: lighten($lighter-text-color, 7%);
 | 
				
			||||||
 | 
					      background-color: transparent;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &.active {
 | 
					    &.active {
 | 
				
			||||||
| 
						 | 
					@ -186,7 +201,8 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.text-icon-button {
 | 
					.text-icon-button {
 | 
				
			||||||
  color: $lighter-text-color;
 | 
					  color: $lighter-text-color;
 | 
				
			||||||
  border: none;
 | 
					  border: 0;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
  background: transparent;
 | 
					  background: transparent;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  font-weight: 600;
 | 
					  font-weight: 600;
 | 
				
			||||||
| 
						 | 
					@ -194,17 +210,25 @@
 | 
				
			||||||
  padding: 0 3px;
 | 
					  padding: 0 3px;
 | 
				
			||||||
  line-height: 27px;
 | 
					  line-height: 27px;
 | 
				
			||||||
  outline: 0;
 | 
					  outline: 0;
 | 
				
			||||||
  transition: color 100ms ease-in;
 | 
					  transition: all 100ms ease-in;
 | 
				
			||||||
 | 
					  transition-property: background-color, color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover,
 | 
					  &:hover,
 | 
				
			||||||
  &:active,
 | 
					  &:active,
 | 
				
			||||||
  &:focus {
 | 
					  &:focus {
 | 
				
			||||||
    color: darken($lighter-text-color, 7%);
 | 
					    color: darken($lighter-text-color, 7%);
 | 
				
			||||||
    transition: color 200ms ease-out;
 | 
					    background-color: rgba($lighter-text-color, 0.15);
 | 
				
			||||||
 | 
					    transition: all 200ms ease-out;
 | 
				
			||||||
 | 
					    transition-property: background-color, color;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:focus {
 | 
				
			||||||
 | 
					    background-color: rgba($lighter-text-color, 0.3);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.disabled {
 | 
					  &.disabled {
 | 
				
			||||||
    color: lighten($lighter-text-color, 20%);
 | 
					    color: lighten($lighter-text-color, 20%);
 | 
				
			||||||
 | 
					    background-color: transparent;
 | 
				
			||||||
    cursor: default;
 | 
					    cursor: default;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,10 +8,11 @@ export default class Column extends React.PureComponent {
 | 
				
			||||||
  static propTypes = {
 | 
					  static propTypes = {
 | 
				
			||||||
    children: PropTypes.node,
 | 
					    children: PropTypes.node,
 | 
				
			||||||
    label: PropTypes.string,
 | 
					    label: PropTypes.string,
 | 
				
			||||||
 | 
					    bindToDocument: PropTypes.bool,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  scrollTop () {
 | 
					  scrollTop () {
 | 
				
			||||||
    const scrollable = this.node.querySelector('.scrollable');
 | 
					    const scrollable = this.props.bindToDocument ? document.scrollingElement : this.node.querySelector('.scrollable');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!scrollable) {
 | 
					    if (!scrollable) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
| 
						 | 
					@ -33,12 +34,20 @@ export default class Column extends React.PureComponent {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentDidMount () {
 | 
					  componentDidMount () {
 | 
				
			||||||
 | 
					    if (this.props.bindToDocument) {
 | 
				
			||||||
 | 
					      document.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
      this.node.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
 | 
					      this.node.addEventListener('wheel', this.handleWheel,  detectPassiveEvents.hasSupport ? { passive: true } : false);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillUnmount () {
 | 
					  componentWillUnmount () {
 | 
				
			||||||
 | 
					    if (this.props.bindToDocument) {
 | 
				
			||||||
 | 
					      document.removeEventListener('wheel', this.handleWheel);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
      this.node.removeEventListener('wheel', this.handleWheel);
 | 
					      this.node.removeEventListener('wheel', this.handleWheel);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { label, children } = this.props;
 | 
					    const { label, children } = this.props;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,6 +2,7 @@ import React from 'react';
 | 
				
			||||||
import { FormattedMessage } from 'react-intl';
 | 
					import { FormattedMessage } from 'react-intl';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import Icon from 'mastodon/components/icon';
 | 
					import Icon from 'mastodon/components/icon';
 | 
				
			||||||
 | 
					import { createPortal } from 'react-dom';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class ColumnBackButton extends React.PureComponent {
 | 
					export default class ColumnBackButton extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +10,10 @@ export default class ColumnBackButton extends React.PureComponent {
 | 
				
			||||||
    router: PropTypes.object,
 | 
					    router: PropTypes.object,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static propTypes = {
 | 
				
			||||||
 | 
					    multiColumn: PropTypes.bool,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  handleClick = () => {
 | 
					  handleClick = () => {
 | 
				
			||||||
    if (window.history && window.history.length === 1) {
 | 
					    if (window.history && window.history.length === 1) {
 | 
				
			||||||
      this.context.router.history.push('/');
 | 
					      this.context.router.history.push('/');
 | 
				
			||||||
| 
						 | 
					@ -18,12 +23,20 @@ export default class ColumnBackButton extends React.PureComponent {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    return (
 | 
					    const { multiColumn } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const component = (
 | 
				
			||||||
      <button onClick={this.handleClick} className='column-back-button'>
 | 
					      <button onClick={this.handleClick} className='column-back-button'>
 | 
				
			||||||
        <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
 | 
					        <Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
 | 
				
			||||||
        <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
 | 
					        <FormattedMessage id='column_back_button.label' defaultMessage='Back' />
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (multiColumn) {
 | 
				
			||||||
 | 
					      return component;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return createPortal(component, document.getElementById('tabs-bar__portal'));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,6 @@
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import { createPortal } from 'react-dom';
 | 
				
			||||||
import classNames from 'classnames';
 | 
					import classNames from 'classnames';
 | 
				
			||||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
 | 
					import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
 | 
				
			||||||
import Icon from 'mastodon/components/icon';
 | 
					import Icon from 'mastodon/components/icon';
 | 
				
			||||||
| 
						 | 
					@ -28,6 +29,7 @@ class ColumnHeader extends React.PureComponent {
 | 
				
			||||||
    showBackButton: PropTypes.bool,
 | 
					    showBackButton: PropTypes.bool,
 | 
				
			||||||
    children: PropTypes.node,
 | 
					    children: PropTypes.node,
 | 
				
			||||||
    pinned: PropTypes.bool,
 | 
					    pinned: PropTypes.bool,
 | 
				
			||||||
 | 
					    placeholder: PropTypes.bool,
 | 
				
			||||||
    onPin: PropTypes.func,
 | 
					    onPin: PropTypes.func,
 | 
				
			||||||
    onMove: PropTypes.func,
 | 
					    onMove: PropTypes.func,
 | 
				
			||||||
    onClick: PropTypes.func,
 | 
					    onClick: PropTypes.func,
 | 
				
			||||||
| 
						 | 
					@ -79,7 +81,7 @@ class ColumnHeader extends React.PureComponent {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage } } = this.props;
 | 
					    const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder } = this.props;
 | 
				
			||||||
    const { collapsed, animating } = this.state;
 | 
					    const { collapsed, animating } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const wrapperClassName = classNames('column-header__wrapper', {
 | 
					    const wrapperClassName = classNames('column-header__wrapper', {
 | 
				
			||||||
| 
						 | 
					@ -146,7 +148,7 @@ class ColumnHeader extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const hasTitle = icon && title;
 | 
					    const hasTitle = icon && title;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    const component = (
 | 
				
			||||||
      <div className={wrapperClassName}>
 | 
					      <div className={wrapperClassName}>
 | 
				
			||||||
        <h1 className={buttonClassName}>
 | 
					        <h1 className={buttonClassName}>
 | 
				
			||||||
          {hasTitle && (
 | 
					          {hasTitle && (
 | 
				
			||||||
| 
						 | 
					@ -172,6 +174,12 @@ class ColumnHeader extends React.PureComponent {
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (multiColumn || placeholder) {
 | 
				
			||||||
 | 
					      return component;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return createPortal(component, document.getElementById('tabs-bar__portal'));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,6 +56,7 @@ class AccountGallery extends ImmutablePureComponent {
 | 
				
			||||||
    isLoading: PropTypes.bool,
 | 
					    isLoading: PropTypes.bool,
 | 
				
			||||||
    hasMore: PropTypes.bool,
 | 
					    hasMore: PropTypes.bool,
 | 
				
			||||||
    isAccount: PropTypes.bool,
 | 
					    isAccount: PropTypes.bool,
 | 
				
			||||||
 | 
					    multiColumn: PropTypes.bool,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  state = {
 | 
					  state = {
 | 
				
			||||||
| 
						 | 
					@ -116,7 +117,7 @@ class AccountGallery extends ImmutablePureComponent {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount } = this.props;
 | 
					    const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn } = this.props;
 | 
				
			||||||
    const { width } = this.state;
 | 
					    const { width } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!isAccount) {
 | 
					    if (!isAccount) {
 | 
				
			||||||
| 
						 | 
					@ -143,7 +144,7 @@ class AccountGallery extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column>
 | 
					      <Column>
 | 
				
			||||||
        <ColumnBackButton />
 | 
					        <ColumnBackButton multiColumn={multiColumn} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={shouldUpdateScroll}>
 | 
					        <ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={shouldUpdateScroll}>
 | 
				
			||||||
          <div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
 | 
					          <div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -100,7 +100,7 @@ class AccountTimeline extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column>
 | 
					      <Column>
 | 
				
			||||||
        <ColumnBackButton />
 | 
					        <ColumnBackButton multiColumn={multiColumn} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <StatusList
 | 
					        <StatusList
 | 
				
			||||||
          prepend={<HeaderContainer accountId={this.props.params.accountId} />}
 | 
					          prepend={<HeaderContainer accountId={this.props.params.accountId} />}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,7 @@ class Blocks extends ImmutablePureComponent {
 | 
				
			||||||
    const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />;
 | 
					    const emptyMessage = <FormattedMessage id='empty_column.blocks' defaultMessage="You haven't blocked any users yet." />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column icon='ban' heading={intl.formatMessage(messages.heading)}>
 | 
					      <Column bindToDocument={!multiColumn} icon='ban' heading={intl.formatMessage(messages.heading)}>
 | 
				
			||||||
        <ColumnBackButtonSlim />
 | 
					        <ColumnBackButtonSlim />
 | 
				
			||||||
        <ScrollableList
 | 
					        <ScrollableList
 | 
				
			||||||
          scrollKey='blocks'
 | 
					          scrollKey='blocks'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -105,7 +105,7 @@ class CommunityTimeline extends React.PureComponent {
 | 
				
			||||||
    const pinned = !!columnId;
 | 
					    const pinned = !!columnId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
 | 
					      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
 | 
				
			||||||
        <ColumnHeader
 | 
					        <ColumnHeader
 | 
				
			||||||
          icon='users'
 | 
					          icon='users'
 | 
				
			||||||
          active={hasUnread}
 | 
					          active={hasUnread}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,12 @@
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const iconStyle = {
 | 
				
			||||||
 | 
					  height: null,
 | 
				
			||||||
 | 
					  lineHeight: '27px',
 | 
				
			||||||
 | 
					  width: `${18 * 1.28571429}px`,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class TextIconButton extends React.PureComponent {
 | 
					export default class TextIconButton extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static propTypes = {
 | 
					  static propTypes = {
 | 
				
			||||||
| 
						 | 
					@ -20,7 +26,14 @@ export default class TextIconButton extends React.PureComponent {
 | 
				
			||||||
    const { label, title, active, ariaControls } = this.props;
 | 
					    const { label, title, active, ariaControls } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <button title={title} aria-label={title} className={`text-icon-button ${active ? 'active' : ''}`} aria-expanded={active} onClick={this.handleClick} aria-controls={ariaControls}>
 | 
					      <button
 | 
				
			||||||
 | 
					        title={title}
 | 
				
			||||||
 | 
					        aria-label={title}
 | 
				
			||||||
 | 
					        className={`text-icon-button ${active ? 'active' : ''}`}
 | 
				
			||||||
 | 
					        aria-expanded={active}
 | 
				
			||||||
 | 
					        onClick={this.handleClick}
 | 
				
			||||||
 | 
					        aria-controls={ariaControls} style={iconStyle}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
        {label}
 | 
					        {label}
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -75,7 +75,7 @@ class DirectTimeline extends React.PureComponent {
 | 
				
			||||||
    const pinned = !!columnId;
 | 
					    const pinned = !!columnId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
 | 
					      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
 | 
				
			||||||
        <ColumnHeader
 | 
					        <ColumnHeader
 | 
				
			||||||
          icon='envelope'
 | 
					          icon='envelope'
 | 
				
			||||||
          active={hasUnread}
 | 
					          active={hasUnread}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,7 +58,7 @@ class Blocks extends ImmutablePureComponent {
 | 
				
			||||||
    const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />;
 | 
					    const emptyMessage = <FormattedMessage id='empty_column.domain_blocks' defaultMessage='There are no hidden domains yet.' />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column icon='minus-circle' heading={intl.formatMessage(messages.heading)}>
 | 
					      <Column bindToDocument={!multiColumn} icon='minus-circle' heading={intl.formatMessage(messages.heading)}>
 | 
				
			||||||
        <ColumnBackButtonSlim />
 | 
					        <ColumnBackButtonSlim />
 | 
				
			||||||
        <ScrollableList
 | 
					        <ScrollableList
 | 
				
			||||||
          scrollKey='domain_blocks'
 | 
					          scrollKey='domain_blocks'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -74,7 +74,7 @@ class Favourites extends ImmutablePureComponent {
 | 
				
			||||||
    const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite toots yet. When you favourite one, it will show up here." />;
 | 
					    const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite toots yet. When you favourite one, it will show up here." />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column ref={this.setRef} label={intl.formatMessage(messages.heading)}>
 | 
					      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
 | 
				
			||||||
        <ColumnHeader
 | 
					        <ColumnHeader
 | 
				
			||||||
          icon='star'
 | 
					          icon='star'
 | 
				
			||||||
          title={intl.formatMessage(messages.heading)}
 | 
					          title={intl.formatMessage(messages.heading)}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,7 +51,7 @@ class Favourites extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column>
 | 
					      <Column>
 | 
				
			||||||
        <ColumnBackButton />
 | 
					        <ColumnBackButton multiColumn={multiColumn} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ScrollableList
 | 
					        <ScrollableList
 | 
				
			||||||
          scrollKey='favourites'
 | 
					          scrollKey='favourites'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,7 @@ class FollowRequests extends ImmutablePureComponent {
 | 
				
			||||||
    const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
 | 
					    const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column icon='user-plus' heading={intl.formatMessage(messages.heading)}>
 | 
					      <Column bindToDocument={!multiColumn} icon='user-plus' heading={intl.formatMessage(messages.heading)}>
 | 
				
			||||||
        <ColumnBackButtonSlim />
 | 
					        <ColumnBackButtonSlim />
 | 
				
			||||||
        <ScrollableList
 | 
					        <ScrollableList
 | 
				
			||||||
          scrollKey='follow_requests'
 | 
					          scrollKey='follow_requests'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,7 +78,7 @@ class Followers extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column>
 | 
					      <Column>
 | 
				
			||||||
        <ColumnBackButton />
 | 
					        <ColumnBackButton multiColumn={multiColumn} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ScrollableList
 | 
					        <ScrollableList
 | 
				
			||||||
          scrollKey='followers'
 | 
					          scrollKey='followers'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -78,7 +78,7 @@ class Following extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column>
 | 
					      <Column>
 | 
				
			||||||
        <ColumnBackButton />
 | 
					        <ColumnBackButton multiColumn={multiColumn} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ScrollableList
 | 
					        <ScrollableList
 | 
				
			||||||
          scrollKey='following'
 | 
					          scrollKey='following'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -148,7 +148,7 @@ class GettingStarted extends ImmutablePureComponent {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column label={intl.formatMessage(messages.menu)}>
 | 
					      <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.menu)}>
 | 
				
			||||||
        {multiColumn && <div className='column-header__wrapper'>
 | 
					        {multiColumn && <div className='column-header__wrapper'>
 | 
				
			||||||
          <h1 className='column-header'>
 | 
					          <h1 className='column-header'>
 | 
				
			||||||
            <button>
 | 
					            <button>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -135,7 +135,7 @@ class HashtagTimeline extends React.PureComponent {
 | 
				
			||||||
    const pinned = !!columnId;
 | 
					    const pinned = !!columnId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column ref={this.setRef} label={`#${id}`}>
 | 
					      <Column bindToDocument={!multiColumn} ref={this.setRef} label={`#${id}`}>
 | 
				
			||||||
        <ColumnHeader
 | 
					        <ColumnHeader
 | 
				
			||||||
          icon='hashtag'
 | 
					          icon='hashtag'
 | 
				
			||||||
          active={hasUnread}
 | 
					          active={hasUnread}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -98,7 +98,7 @@ class HomeTimeline extends React.PureComponent {
 | 
				
			||||||
    const pinned = !!columnId;
 | 
					    const pinned = !!columnId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
 | 
					      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
 | 
				
			||||||
        <ColumnHeader
 | 
					        <ColumnHeader
 | 
				
			||||||
          icon='home'
 | 
					          icon='home'
 | 
				
			||||||
          active={hasUnread}
 | 
					          active={hasUnread}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,10 +18,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { intl } = this.props;
 | 
					    const { intl, multiColumn } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column icon='question' heading={intl.formatMessage(messages.heading)}>
 | 
					      <Column bindToDocument={!multiColumn} icon='question' heading={intl.formatMessage(messages.heading)}>
 | 
				
			||||||
        <ColumnBackButtonSlim />
 | 
					        <ColumnBackButtonSlim />
 | 
				
			||||||
        <div className='keyboard-shortcuts scrollable optionally-scrollable'>
 | 
					        <div className='keyboard-shortcuts scrollable optionally-scrollable'>
 | 
				
			||||||
          <table>
 | 
					          <table>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@ const messages = defineMessages({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
  value: state.getIn(['listEditor', 'title']),
 | 
					  value: state.getIn(['listEditor', 'title']),
 | 
				
			||||||
  disabled: !state.getIn(['listEditor', 'isChanged']),
 | 
					  disabled: !state.getIn(['listEditor', 'isChanged']) || !state.getIn(['listEditor', 'title']),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapDispatchToProps = dispatch => ({
 | 
					const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -148,14 +148,14 @@ class ListTimeline extends React.PureComponent {
 | 
				
			||||||
    } else if (list === false) {
 | 
					    } else if (list === false) {
 | 
				
			||||||
      return (
 | 
					      return (
 | 
				
			||||||
        <Column>
 | 
					        <Column>
 | 
				
			||||||
          <ColumnBackButton />
 | 
					          <ColumnBackButton multiColumn={multiColumn} />
 | 
				
			||||||
          <MissingIndicator />
 | 
					          <MissingIndicator />
 | 
				
			||||||
        </Column>
 | 
					        </Column>
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column ref={this.setRef} label={title}>
 | 
					      <Column bindToDocument={!multiColumn} ref={this.setRef} label={title}>
 | 
				
			||||||
        <ColumnHeader
 | 
					        <ColumnHeader
 | 
				
			||||||
          icon='list-ul'
 | 
					          icon='list-ul'
 | 
				
			||||||
          active={hasUnread}
 | 
					          active={hasUnread}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -66,7 +66,7 @@ class NewListForm extends React.PureComponent {
 | 
				
			||||||
        </label>
 | 
					        </label>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <IconButton
 | 
					        <IconButton
 | 
				
			||||||
          disabled={disabled}
 | 
					          disabled={disabled || !value}
 | 
				
			||||||
          icon='plus'
 | 
					          icon='plus'
 | 
				
			||||||
          title={title}
 | 
					          title={title}
 | 
				
			||||||
          onClick={this.handleClick}
 | 
					          onClick={this.handleClick}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,7 +61,7 @@ class Lists extends ImmutablePureComponent {
 | 
				
			||||||
    const emptyMessage = <FormattedMessage id='empty_column.lists' defaultMessage="You don't have any lists yet. When you create one, it will show up here." />;
 | 
					    const emptyMessage = <FormattedMessage id='empty_column.lists' defaultMessage="You don't have any lists yet. When you create one, it will show up here." />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column icon='list-ul' heading={intl.formatMessage(messages.heading)}>
 | 
					      <Column bindToDocument={!multiColumn} icon='list-ul' heading={intl.formatMessage(messages.heading)}>
 | 
				
			||||||
        <ColumnBackButtonSlim />
 | 
					        <ColumnBackButtonSlim />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <NewListForm />
 | 
					        <NewListForm />
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,7 @@ class Mutes extends ImmutablePureComponent {
 | 
				
			||||||
    const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />;
 | 
					    const emptyMessage = <FormattedMessage id='empty_column.mutes' defaultMessage="You haven't muted any users yet." />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column icon='volume-off' heading={intl.formatMessage(messages.heading)}>
 | 
					      <Column bindToDocument={!multiColumn} icon='volume-off' heading={intl.formatMessage(messages.heading)}>
 | 
				
			||||||
        <ColumnBackButtonSlim />
 | 
					        <ColumnBackButtonSlim />
 | 
				
			||||||
        <ScrollableList
 | 
					        <ScrollableList
 | 
				
			||||||
          scrollKey='mutes'
 | 
					          scrollKey='mutes'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -198,7 +198,7 @@ class Notifications extends React.PureComponent {
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column ref={this.setColumnRef} label={intl.formatMessage(messages.title)}>
 | 
					      <Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.title)}>
 | 
				
			||||||
        <ColumnHeader
 | 
					        <ColumnHeader
 | 
				
			||||||
          icon='bell'
 | 
					          icon='bell'
 | 
				
			||||||
          active={isUnread}
 | 
					          active={isUnread}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -47,7 +47,7 @@ class PinnedStatuses extends ImmutablePureComponent {
 | 
				
			||||||
    const { intl, shouldUpdateScroll, statusIds, hasMore, multiColumn } = this.props;
 | 
					    const { intl, shouldUpdateScroll, statusIds, hasMore, multiColumn } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}>
 | 
					      <Column bindToDocument={!multiColumn} icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}>
 | 
				
			||||||
        <ColumnBackButtonSlim />
 | 
					        <ColumnBackButtonSlim />
 | 
				
			||||||
        <StatusList
 | 
					        <StatusList
 | 
				
			||||||
          statusIds={statusIds}
 | 
					          statusIds={statusIds}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -105,7 +105,7 @@ class PublicTimeline extends React.PureComponent {
 | 
				
			||||||
    const pinned = !!columnId;
 | 
					    const pinned = !!columnId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column ref={this.setRef} label={intl.formatMessage(messages.title)}>
 | 
					      <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
 | 
				
			||||||
        <ColumnHeader
 | 
					        <ColumnHeader
 | 
				
			||||||
          icon='globe'
 | 
					          icon='globe'
 | 
				
			||||||
          active={hasUnread}
 | 
					          active={hasUnread}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -51,7 +51,7 @@ class Reblogs extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column>
 | 
					      <Column>
 | 
				
			||||||
        <ColumnBackButton />
 | 
					        <ColumnBackButton multiColumn={multiColumn} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ScrollableList
 | 
					        <ScrollableList
 | 
				
			||||||
          scrollKey='reblogs'
 | 
					          scrollKey='reblogs'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -146,6 +146,7 @@ class Status extends ImmutablePureComponent {
 | 
				
			||||||
    descendantsIds: ImmutablePropTypes.list,
 | 
					    descendantsIds: ImmutablePropTypes.list,
 | 
				
			||||||
    intl: PropTypes.object.isRequired,
 | 
					    intl: PropTypes.object.isRequired,
 | 
				
			||||||
    askReplyConfirmation: PropTypes.bool,
 | 
					    askReplyConfirmation: PropTypes.bool,
 | 
				
			||||||
 | 
					    multiColumn: PropTypes.bool,
 | 
				
			||||||
    domain: PropTypes.string.isRequired,
 | 
					    domain: PropTypes.string.isRequired,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -437,13 +438,13 @@ class Status extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    let ancestors, descendants;
 | 
					    let ancestors, descendants;
 | 
				
			||||||
    const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain } = this.props;
 | 
					    const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props;
 | 
				
			||||||
    const { fullscreen } = this.state;
 | 
					    const { fullscreen } = this.state;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (status === null) {
 | 
					    if (status === null) {
 | 
				
			||||||
      return (
 | 
					      return (
 | 
				
			||||||
        <Column>
 | 
					        <Column>
 | 
				
			||||||
          <ColumnBackButton />
 | 
					          <ColumnBackButton multiColumn={multiColumn} />
 | 
				
			||||||
          <MissingIndicator />
 | 
					          <MissingIndicator />
 | 
				
			||||||
        </Column>
 | 
					        </Column>
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
| 
						 | 
					@ -470,9 +471,10 @@ class Status extends ImmutablePureComponent {
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column label={intl.formatMessage(messages.detailedStatus)}>
 | 
					      <Column bindToDocument={!multiColumn} label={intl.formatMessage(messages.detailedStatus)}>
 | 
				
			||||||
        <ColumnHeader
 | 
					        <ColumnHeader
 | 
				
			||||||
          showBackButton
 | 
					          showBackButton
 | 
				
			||||||
 | 
					          multiColumn={multiColumn}
 | 
				
			||||||
          extraButton={(
 | 
					          extraButton={(
 | 
				
			||||||
            <button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button>
 | 
					            <button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ export default class ColumnLoading extends ImmutablePureComponent {
 | 
				
			||||||
    let { title, icon } = this.props;
 | 
					    let { title, icon } = this.props;
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <Column>
 | 
					      <Column>
 | 
				
			||||||
        <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} />
 | 
					        <ColumnHeader icon={icon} title={title} multiColumn={false} focusable={false} placeholder />
 | 
				
			||||||
        <div className='scrollable' />
 | 
					        <div className='scrollable' />
 | 
				
			||||||
      </Column>
 | 
					      </Column>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -73,9 +73,13 @@ class TabsBar extends React.PureComponent {
 | 
				
			||||||
    const { intl: { formatMessage } } = this.props;
 | 
					    const { intl: { formatMessage } } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
 | 
					      <div className='tabs-bar__wrapper'>
 | 
				
			||||||
        <nav className='tabs-bar' ref={this.setRef}>
 | 
					        <nav className='tabs-bar' ref={this.setRef}>
 | 
				
			||||||
          {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
 | 
					          {links.map(link => React.cloneElement(link, { key: link.props.to, onClick: this.handleClick, 'aria-label': formatMessage({ id: link.props['data-preview-title-id'] }) }))}
 | 
				
			||||||
        </nav>
 | 
					        </nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div id='tabs-bar__portal' />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -153,9 +153,9 @@ const sortHashtagsByUse = (state, tags) => {
 | 
				
			||||||
    if (usedA === usedB) {
 | 
					    if (usedA === usedB) {
 | 
				
			||||||
      return 0;
 | 
					      return 0;
 | 
				
			||||||
    } else if (usedA && !usedB) {
 | 
					    } else if (usedA && !usedB) {
 | 
				
			||||||
      return 1;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      return -1;
 | 
					      return -1;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      return 1;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,7 +39,7 @@ body {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &.layout-single-column {
 | 
					    &.layout-single-column {
 | 
				
			||||||
      height: auto;
 | 
					      height: auto;
 | 
				
			||||||
      min-height: 100%;
 | 
					      min-height: 100vh;
 | 
				
			||||||
      overflow-y: scroll;
 | 
					      overflow-y: scroll;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -129,19 +129,28 @@
 | 
				
			||||||
  padding: 0;
 | 
					  padding: 0;
 | 
				
			||||||
  color: $action-button-color;
 | 
					  color: $action-button-color;
 | 
				
			||||||
  border: 0;
 | 
					  border: 0;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
  background: transparent;
 | 
					  background: transparent;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  transition: color 100ms ease-in;
 | 
					  transition: all 100ms ease-in;
 | 
				
			||||||
 | 
					  transition-property: background-color, color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover,
 | 
					  &:hover,
 | 
				
			||||||
  &:active,
 | 
					  &:active,
 | 
				
			||||||
  &:focus {
 | 
					  &:focus {
 | 
				
			||||||
    color: lighten($action-button-color, 7%);
 | 
					    color: lighten($action-button-color, 7%);
 | 
				
			||||||
    transition: color 200ms ease-out;
 | 
					    background-color: rgba($action-button-color, 0.15);
 | 
				
			||||||
 | 
					    transition: all 200ms ease-out;
 | 
				
			||||||
 | 
					    transition-property: background-color, color;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:focus {
 | 
				
			||||||
 | 
					    background-color: rgba($action-button-color, 0.3);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.disabled {
 | 
					  &.disabled {
 | 
				
			||||||
    color: darken($action-button-color, 13%);
 | 
					    color: darken($action-button-color, 13%);
 | 
				
			||||||
 | 
					    background-color: transparent;
 | 
				
			||||||
    cursor: default;
 | 
					    cursor: default;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -166,10 +175,16 @@
 | 
				
			||||||
    &:active,
 | 
					    &:active,
 | 
				
			||||||
    &:focus {
 | 
					    &:focus {
 | 
				
			||||||
      color: darken($lighter-text-color, 7%);
 | 
					      color: darken($lighter-text-color, 7%);
 | 
				
			||||||
 | 
					      background-color: rgba($lighter-text-color, 0.15);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &:focus {
 | 
				
			||||||
 | 
					      background-color: rgba($lighter-text-color, 0.3);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &.disabled {
 | 
					    &.disabled {
 | 
				
			||||||
      color: lighten($lighter-text-color, 7%);
 | 
					      color: lighten($lighter-text-color, 7%);
 | 
				
			||||||
 | 
					      background-color: transparent;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &.active {
 | 
					    &.active {
 | 
				
			||||||
| 
						 | 
					@ -197,6 +212,7 @@
 | 
				
			||||||
.text-icon-button {
 | 
					.text-icon-button {
 | 
				
			||||||
  color: $lighter-text-color;
 | 
					  color: $lighter-text-color;
 | 
				
			||||||
  border: 0;
 | 
					  border: 0;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
  background: transparent;
 | 
					  background: transparent;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
  font-weight: 600;
 | 
					  font-weight: 600;
 | 
				
			||||||
| 
						 | 
					@ -204,17 +220,25 @@
 | 
				
			||||||
  padding: 0 3px;
 | 
					  padding: 0 3px;
 | 
				
			||||||
  line-height: 27px;
 | 
					  line-height: 27px;
 | 
				
			||||||
  outline: 0;
 | 
					  outline: 0;
 | 
				
			||||||
  transition: color 100ms ease-in;
 | 
					  transition: all 100ms ease-in;
 | 
				
			||||||
 | 
					  transition-property: background-color, color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &:hover,
 | 
					  &:hover,
 | 
				
			||||||
  &:active,
 | 
					  &:active,
 | 
				
			||||||
  &:focus {
 | 
					  &:focus {
 | 
				
			||||||
    color: darken($lighter-text-color, 7%);
 | 
					    color: darken($lighter-text-color, 7%);
 | 
				
			||||||
    transition: color 200ms ease-out;
 | 
					    background-color: rgba($lighter-text-color, 0.15);
 | 
				
			||||||
 | 
					    transition: all 200ms ease-out;
 | 
				
			||||||
 | 
					    transition-property: background-color, color;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  &:focus {
 | 
				
			||||||
 | 
					    background-color: rgba($lighter-text-color, 0.3);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  &.disabled {
 | 
					  &.disabled {
 | 
				
			||||||
    color: lighten($lighter-text-color, 20%);
 | 
					    color: lighten($lighter-text-color, 20%);
 | 
				
			||||||
 | 
					    background-color: transparent;
 | 
				
			||||||
    cursor: default;
 | 
					    cursor: default;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -604,7 +628,8 @@
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .icon-button {
 | 
					    .icon-button,
 | 
				
			||||||
 | 
					    .text-icon-button {
 | 
				
			||||||
      box-sizing: content-box;
 | 
					      box-sizing: content-box;
 | 
				
			||||||
      padding: 0 3px;
 | 
					      padding: 0 3px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -731,7 +756,7 @@
 | 
				
			||||||
    white-space: pre-wrap;
 | 
					    white-space: pre-wrap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &:last-child {
 | 
					    &:last-child {
 | 
				
			||||||
      margin-bottom: 2px;
 | 
					      margin-bottom: 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1852,6 +1877,26 @@ a.account__display-name {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tabs-bar__wrapper {
 | 
				
			||||||
 | 
					  background: darken($ui-base-color, 8%);
 | 
				
			||||||
 | 
					  position: sticky;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  z-index: 2;
 | 
				
			||||||
 | 
					  padding-top: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @media screen and (min-width: $no-gap-breakpoint) {
 | 
				
			||||||
 | 
					    padding-top: 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .tabs-bar {
 | 
				
			||||||
 | 
					    margin-bottom: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @media screen and (min-width: $no-gap-breakpoint) {
 | 
				
			||||||
 | 
					      margin-bottom: 10px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.react-swipeable-view-container {
 | 
					.react-swipeable-view-container {
 | 
				
			||||||
  &,
 | 
					  &,
 | 
				
			||||||
  .columns-area,
 | 
					  .columns-area,
 | 
				
			||||||
| 
						 | 
					@ -1949,9 +1994,6 @@ a.account__display-name {
 | 
				
			||||||
  background: lighten($ui-base-color, 8%);
 | 
					  background: lighten($ui-base-color, 8%);
 | 
				
			||||||
  flex: 0 0 auto;
 | 
					  flex: 0 0 auto;
 | 
				
			||||||
  overflow-y: auto;
 | 
					  overflow-y: auto;
 | 
				
			||||||
  position: sticky;
 | 
					 | 
				
			||||||
  top: 0;
 | 
					 | 
				
			||||||
  z-index: 3;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.tabs-bar__link {
 | 
					.tabs-bar__link {
 | 
				
			||||||
| 
						 | 
					@ -2014,6 +2056,14 @@ a.account__display-name {
 | 
				
			||||||
    padding: 0;
 | 
					    padding: 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //.column {
 | 
				
			||||||
 | 
					  //  margin-top: 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  //  @media screen and (min-width: $no-gap-breakpoint) {
 | 
				
			||||||
 | 
					  //    margin-top: 10px;
 | 
				
			||||||
 | 
					  //  }
 | 
				
			||||||
 | 
					  //}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .autosuggest-textarea__textarea {
 | 
					  .autosuggest-textarea__textarea {
 | 
				
			||||||
    font-size: 16px;
 | 
					    font-size: 16px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					@ -2039,6 +2089,7 @@ a.account__display-name {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @media screen and (min-width: $no-gap-breakpoint) {
 | 
					  @media screen and (min-width: $no-gap-breakpoint) {
 | 
				
			||||||
    padding: 10px 0;
 | 
					    padding: 10px 0;
 | 
				
			||||||
 | 
					    padding-top: 0;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @media screen and (min-width: 630px) {
 | 
					  @media screen and (min-width: 630px) {
 | 
				
			||||||
| 
						 | 
					@ -2153,13 +2204,11 @@ a.account__display-name {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@media screen and (min-width: $no-gap-breakpoint) {
 | 
					@media screen and (min-width: $no-gap-breakpoint) {
 | 
				
			||||||
  .tabs-bar {
 | 
					  .tabs-bar {
 | 
				
			||||||
    margin: 10px auto;
 | 
					 | 
				
			||||||
    margin-bottom: 0;
 | 
					 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .react-swipeable-view-container .columns-area--mobile {
 | 
					  .react-swipeable-view-container .columns-area--mobile {
 | 
				
			||||||
    height: calc(100% - 20px) !important;
 | 
					    height: calc(100% - 10px) !important;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .getting-started__wrapper,
 | 
					  .getting-started__wrapper,
 | 
				
			||||||
| 
						 | 
					@ -2387,6 +2436,8 @@ a.account__display-name {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.column-back-button {
 | 
					.column-back-button {
 | 
				
			||||||
 | 
					  box-sizing: border-box;
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
  background: lighten($ui-base-color, 4%);
 | 
					  background: lighten($ui-base-color, 4%);
 | 
				
			||||||
  color: $highlight-text-color;
 | 
					  color: $highlight-text-color;
 | 
				
			||||||
  cursor: pointer;
 | 
					  cursor: pointer;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,7 +37,7 @@ class FeedManager
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def unpush_from_home(account, status)
 | 
					  def unpush_from_home(account, status)
 | 
				
			||||||
    return false unless remove_from_feed(:home, account.id, status)
 | 
					    return false unless remove_from_feed(:home, account.id, status, account.user&.aggregates_reblogs?)
 | 
				
			||||||
    redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
 | 
					    redis.publish("timeline:#{account.id}", Oj.dump(event: :delete, payload: status.id.to_s))
 | 
				
			||||||
    true
 | 
					    true
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -56,7 +56,7 @@ class FeedManager
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def unpush_from_list(list, status)
 | 
					  def unpush_from_list(list, status)
 | 
				
			||||||
    return false unless remove_from_feed(:list, list.id, status)
 | 
					    return false unless remove_from_feed(:list, list.id, status, list.account.user&.aggregates_reblogs?)
 | 
				
			||||||
    redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s))
 | 
					    redis.publish("timeline:list:#{list.id}", Oj.dump(event: :delete, payload: status.id.to_s))
 | 
				
			||||||
    true
 | 
					    true
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					@ -120,7 +120,7 @@ class FeedManager
 | 
				
			||||||
    oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
 | 
					    oldest_home_score = redis.zrange(timeline_key, 0, 0, with_scores: true)&.first&.last&.to_i || 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    from_account.statuses.select('id, reblog_of_id').where('id > ?', oldest_home_score).reorder(nil).find_each do |status|
 | 
					    from_account.statuses.select('id, reblog_of_id').where('id > ?', oldest_home_score).reorder(nil).find_each do |status|
 | 
				
			||||||
      remove_from_feed(:home, into_account.id, status)
 | 
					      remove_from_feed(:home, into_account.id, status, into_account.user&.aggregates_reblogs?)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -316,10 +316,11 @@ class FeedManager
 | 
				
			||||||
  # with reblogs, and returning true if a status was removed. As with
 | 
					  # with reblogs, and returning true if a status was removed. As with
 | 
				
			||||||
  # `add_to_feed`, this does not trigger push updates, so callers must
 | 
					  # `add_to_feed`, this does not trigger push updates, so callers must
 | 
				
			||||||
  # do so if appropriate.
 | 
					  # do so if appropriate.
 | 
				
			||||||
  def remove_from_feed(timeline_type, account_id, status)
 | 
					  def remove_from_feed(timeline_type, account_id, status, aggregate_reblogs = true)
 | 
				
			||||||
    timeline_key = key(timeline_type, account_id)
 | 
					    timeline_key = key(timeline_type, account_id)
 | 
				
			||||||
 | 
					    reblog_key   = key(timeline_type, account_id, 'reblogs')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if status.reblog?
 | 
					    if status.reblog? && (aggregate_reblogs.nil? || aggregate_reblogs)
 | 
				
			||||||
      # 1. If the reblogging status is not in the feed, stop.
 | 
					      # 1. If the reblogging status is not in the feed, stop.
 | 
				
			||||||
      status_rank = redis.zrevrank(timeline_key, status.id)
 | 
					      status_rank = redis.zrevrank(timeline_key, status.id)
 | 
				
			||||||
      return false if status_rank.nil?
 | 
					      return false if status_rank.nil?
 | 
				
			||||||
| 
						 | 
					@ -328,6 +329,7 @@ class FeedManager
 | 
				
			||||||
      reblog_set_key = key(timeline_type, account_id, "reblogs:#{status.reblog_of_id}")
 | 
					      reblog_set_key = key(timeline_type, account_id, "reblogs:#{status.reblog_of_id}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      redis.srem(reblog_set_key, status.id)
 | 
					      redis.srem(reblog_set_key, status.id)
 | 
				
			||||||
 | 
					      redis.zrem(reblog_key, status.reblog_of_id)
 | 
				
			||||||
      # 3. Re-insert another reblog or original into the feed if one
 | 
					      # 3. Re-insert another reblog or original into the feed if one
 | 
				
			||||||
      # remains in the set. We could pick a random element, but this
 | 
					      # remains in the set. We could pick a random element, but this
 | 
				
			||||||
      # set should generally be small, and it seems ideal to show the
 | 
					      # set should generally be small, and it seems ideal to show the
 | 
				
			||||||
| 
						 | 
					@ -335,12 +337,14 @@ class FeedManager
 | 
				
			||||||
      other_reblog = redis.smembers(reblog_set_key).map(&:to_i).min
 | 
					      other_reblog = redis.smembers(reblog_set_key).map(&:to_i).min
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      redis.zadd(timeline_key, other_reblog, other_reblog) if other_reblog
 | 
					      redis.zadd(timeline_key, other_reblog, other_reblog) if other_reblog
 | 
				
			||||||
 | 
					      redis.zadd(reblog_key, other_reblog, status.reblog_of_id) if other_reblog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      # 4. Remove the reblogging status from the feed (as normal)
 | 
					      # 4. Remove the reblogging status from the feed (as normal)
 | 
				
			||||||
      # (outside conditional)
 | 
					      # (outside conditional)
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      # If the original is getting deleted, no use for reblog references
 | 
					      # If the original is getting deleted, no use for reblog references
 | 
				
			||||||
      redis.del(key(timeline_type, account_id, "reblogs:#{status.id}"))
 | 
					      redis.del(key(timeline_type, account_id, "reblogs:#{status.id}"))
 | 
				
			||||||
 | 
					      redis.zrem(reblog_key, status.id)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    redis.zrem(timeline_key, status.id)
 | 
					    redis.zrem(timeline_key, status.id)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,6 +7,7 @@
 | 
				
			||||||
#  name       :string           default(""), not null
 | 
					#  name       :string           default(""), not null
 | 
				
			||||||
#  created_at :datetime         not null
 | 
					#  created_at :datetime         not null
 | 
				
			||||||
#  updated_at :datetime         not null
 | 
					#  updated_at :datetime         not null
 | 
				
			||||||
 | 
					#  score      :integer
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Tag < ApplicationRecord
 | 
					class Tag < ApplicationRecord
 | 
				
			||||||
| 
						 | 
					@ -75,10 +76,12 @@ class Tag < ApplicationRecord
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def search_for(term, limit = 5, offset = 0)
 | 
					    def search_for(term, limit = 5, offset = 0)
 | 
				
			||||||
      pattern = sanitize_sql_like(normalize(term.strip)) + '%'
 | 
					      normalized_term = normalize(term.strip).mb_chars.downcase.to_s
 | 
				
			||||||
 | 
					      pattern         = sanitize_sql_like(normalized_term) + '%'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      Tag.where(arel_table[:name].lower.matches(pattern.mb_chars.downcase.to_s))
 | 
					      Tag.where(arel_table[:name].lower.matches(pattern))
 | 
				
			||||||
         .order(:name)
 | 
					         .where(arel_table[:score].gt(0).or(arel_table[:name].lower.eq(normalized_term)))
 | 
				
			||||||
 | 
					         .order(Arel.sql('length(name) ASC, score DESC, name ASC'))
 | 
				
			||||||
         .limit(limit)
 | 
					         .limit(limit)
 | 
				
			||||||
         .offset(offset)
 | 
					         .offset(offset)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,12 +48,17 @@ class TrendingTags
 | 
				
			||||||
        redis.zrem(key, tag_id.to_s)
 | 
					        redis.zrem(key, tag_id.to_s)
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        score = ((observed - expected)**2) / expected
 | 
					        score = ((observed - expected)**2) / expected
 | 
				
			||||||
        redis.zadd(key, score, tag_id.to_s)
 | 
					        added = redis.zadd(key, score, tag_id.to_s)
 | 
				
			||||||
 | 
					        bump_tag_score!(tag_id) if added
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      redis.expire(key, EXPIRE_TRENDS_AFTER)
 | 
					      redis.expire(key, EXPIRE_TRENDS_AFTER)
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def bump_tag_score!(tag_id)
 | 
				
			||||||
 | 
					      Tag.where(id: tag_id).update_all('score = COALESCE(score, 0) + 1')
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def disallowed_hashtags
 | 
					    def disallowed_hashtags
 | 
				
			||||||
      return @disallowed_hashtags if defined?(@disallowed_hashtags)
 | 
					      return @disallowed_hashtags if defined?(@disallowed_hashtags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,6 @@ class WebfingerSerializer < ActiveModel::Serializer
 | 
				
			||||||
    else
 | 
					    else
 | 
				
			||||||
      [
 | 
					      [
 | 
				
			||||||
        { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) },
 | 
					        { rel: 'http://webfinger.net/rel/profile-page', type: 'text/html', href: short_account_url(object) },
 | 
				
			||||||
        { rel: 'http://schemas.google.com/g/2010#updates-from', type: 'application/atom+xml', href: account_url(object, format: 'atom') },
 | 
					 | 
				
			||||||
        { rel: 'self', type: 'application/activity+json', href: account_url(object) },
 | 
					        { rel: 'self', type: 'application/activity+json', href: account_url(object) },
 | 
				
			||||||
        { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
 | 
					        { rel: 'http://ostatus.org/schema/1.0/subscribe', template: "#{authorize_interaction_url}?uri={uri}" },
 | 
				
			||||||
      ]
 | 
					      ]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,6 @@
 | 
				
			||||||
  - if @account.user&.setting_noindex
 | 
					  - if @account.user&.setting_noindex
 | 
				
			||||||
    %meta{ name: 'robots', content: 'noindex, noarchive' }/
 | 
					    %meta{ name: 'robots', content: 'noindex, noarchive' }/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  %link{ rel: 'alternate', type: 'application/atom+xml', href: account_url(@account, format: 'atom') }/
 | 
					 | 
				
			||||||
  %link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/
 | 
					  %link{ rel: 'alternate', type: 'application/rss+xml', href: account_url(@account, format: 'rss') }/
 | 
				
			||||||
  %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
 | 
					  %link{ rel: 'alternate', type: 'application/activity+json', href: ActivityPub::TagManager.instance.uri_for(@account) }/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -74,7 +73,7 @@
 | 
				
			||||||
              - if featured_tag.last_status_at.nil?
 | 
					              - if featured_tag.last_status_at.nil?
 | 
				
			||||||
                = t('accounts.nothing_here')
 | 
					                = t('accounts.nothing_here')
 | 
				
			||||||
              - else
 | 
					              - else
 | 
				
			||||||
                %time{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
 | 
					                %time.formatted{ datetime: featured_tag.last_status_at.iso8601, title: l(featured_tag.last_status_at) }= l featured_tag.last_status_at
 | 
				
			||||||
          .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
 | 
					          .trends__item__current= number_to_human featured_tag.statuses_count, strip_insignificant_zeros: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    = render 'application/sidebar'
 | 
					    = render 'application/sidebar'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,51 +0,0 @@
 | 
				
			||||||
doc = Ox::Document.new(version: '1.0')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
doc << Ox::Element.new('XRD').tap do |xrd|
 | 
					 | 
				
			||||||
  xrd['xmlns'] = 'http://docs.oasis-open.org/ns/xri/xrd-1.0'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  xrd << (Ox::Element.new('Subject') << @account.to_webfinger_s)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if @account.instance_actor?
 | 
					 | 
				
			||||||
    xrd << (Ox::Element.new('Alias') << instance_actor_url)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    xrd << Ox::Element.new('Link').tap do |link|
 | 
					 | 
				
			||||||
      link['rel']      = 'http://webfinger.net/rel/profile-page'
 | 
					 | 
				
			||||||
      link['type']     = 'text/html'
 | 
					 | 
				
			||||||
      link['href']     = about_more_url(instance_actor: true)
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    xrd << Ox::Element.new('Link').tap do |link|
 | 
					 | 
				
			||||||
      link['rel']      = 'self'
 | 
					 | 
				
			||||||
      link['type']     = 'application/activity+json'
 | 
					 | 
				
			||||||
      link['href']     = instance_actor_url
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  else
 | 
					 | 
				
			||||||
    xrd << (Ox::Element.new('Alias') << short_account_url(@account))
 | 
					 | 
				
			||||||
    xrd << (Ox::Element.new('Alias') << account_url(@account))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    xrd << Ox::Element.new('Link').tap do |link|
 | 
					 | 
				
			||||||
      link['rel']      = 'http://webfinger.net/rel/profile-page'
 | 
					 | 
				
			||||||
      link['type']     = 'text/html'
 | 
					 | 
				
			||||||
      link['href']     = short_account_url(@account)
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    xrd << Ox::Element.new('Link').tap do |link|
 | 
					 | 
				
			||||||
      link['rel']      = 'http://schemas.google.com/g/2010#updates-from'
 | 
					 | 
				
			||||||
      link['type']     = 'application/atom+xml'
 | 
					 | 
				
			||||||
      link['href']     = account_url(@account, format: 'atom')
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    xrd << Ox::Element.new('Link').tap do |link|
 | 
					 | 
				
			||||||
      link['rel']      = 'self'
 | 
					 | 
				
			||||||
      link['type']     = 'application/activity+json'
 | 
					 | 
				
			||||||
      link['href']     = account_url(@account)
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    xrd << Ox::Element.new('Link').tap do |link|
 | 
					 | 
				
			||||||
      link['rel']      = 'http://ostatus.org/schema/1.0/subscribe'
 | 
					 | 
				
			||||||
      link['template'] = "#{authorize_interaction_url}?acct={uri}"
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
('<?xml version="1.0" encoding="UTF-8"?>' + Ox.dump(doc, effort: :tolerant)).force_encoding('UTF-8')
 | 
					 | 
				
			||||||
							
								
								
									
										5
									
								
								db/migrate/20190729185330_add_score_to_tags.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20190729185330_add_score_to_tags.rb
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					class AddScoreToTags < ActiveRecord::Migration[5.2]
 | 
				
			||||||
 | 
					  def change
 | 
				
			||||||
 | 
					    add_column :tags, :score, :int
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
# It's strongly recommended that you check this file into your version control system.
 | 
					# It's strongly recommended that you check this file into your version control system.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ActiveRecord::Schema.define(version: 2019_07_28_084117) do
 | 
					ActiveRecord::Schema.define(version: 2019_07_29_185330) do
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  # These are extensions that must be enabled in order to support this database
 | 
					  # These are extensions that must be enabled in order to support this database
 | 
				
			||||||
  enable_extension "plpgsql"
 | 
					  enable_extension "plpgsql"
 | 
				
			||||||
| 
						 | 
					@ -672,6 +672,7 @@ ActiveRecord::Schema.define(version: 2019_07_28_084117) do
 | 
				
			||||||
    t.string "name", default: "", null: false
 | 
					    t.string "name", default: "", null: false
 | 
				
			||||||
    t.datetime "created_at", null: false
 | 
					    t.datetime "created_at", null: false
 | 
				
			||||||
    t.datetime "updated_at", null: false
 | 
					    t.datetime "updated_at", null: false
 | 
				
			||||||
 | 
					    t.integer "score"
 | 
				
			||||||
    t.index "lower((name)::text)", name: "index_tags_on_name_lower", unique: true
 | 
					    t.index "lower((name)::text)", name: "index_tags_on_name_lower", unique: true
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -58,6 +58,7 @@ module Mastodon
 | 
				
			||||||
    option :concurrency, type: :numeric, default: 50, aliases: [:c]
 | 
					    option :concurrency, type: :numeric, default: 50, aliases: [:c]
 | 
				
			||||||
    option :silent, type: :boolean, default: false, aliases: [:s]
 | 
					    option :silent, type: :boolean, default: false, aliases: [:s]
 | 
				
			||||||
    option :format, type: :string, default: 'summary', aliases: [:f]
 | 
					    option :format, type: :string, default: 'summary', aliases: [:f]
 | 
				
			||||||
 | 
					    option :exclude_suspended, type: :boolean, default: false, aliases: [:x]
 | 
				
			||||||
    desc 'crawl [START]', 'Crawl all known peers, optionally beginning at START'
 | 
					    desc 'crawl [START]', 'Crawl all known peers, optionally beginning at START'
 | 
				
			||||||
    long_desc <<-LONG_DESC
 | 
					    long_desc <<-LONG_DESC
 | 
				
			||||||
      Crawl the fediverse by using the Mastodon REST API endpoints that expose
 | 
					      Crawl the fediverse by using the Mastodon REST API endpoints that expose
 | 
				
			||||||
| 
						 | 
					@ -74,6 +75,10 @@ module Mastodon
 | 
				
			||||||
      default (`summary`), a summary of the statistics is returned. The other options
 | 
					      default (`summary`), a summary of the statistics is returned. The other options
 | 
				
			||||||
      are `domains`, which returns a newline-delimited list of all discovered peers,
 | 
					      are `domains`, which returns a newline-delimited list of all discovered peers,
 | 
				
			||||||
      and `json`, which dumps all the aggregated data raw.
 | 
					      and `json`, which dumps all the aggregated data raw.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      The --exclude-suspended (-x) option means that domains that are suspended
 | 
				
			||||||
 | 
					      instance-wide do not appear in the output and are not included in summaries.
 | 
				
			||||||
 | 
					      This also excludes subdomains of any of those domains.
 | 
				
			||||||
    LONG_DESC
 | 
					    LONG_DESC
 | 
				
			||||||
    def crawl(start = nil)
 | 
					    def crawl(start = nil)
 | 
				
			||||||
      stats           = Concurrent::Hash.new
 | 
					      stats           = Concurrent::Hash.new
 | 
				
			||||||
| 
						 | 
					@ -81,11 +86,14 @@ module Mastodon
 | 
				
			||||||
      failed          = Concurrent::AtomicFixnum.new(0)
 | 
					      failed          = Concurrent::AtomicFixnum.new(0)
 | 
				
			||||||
      start_at        = Time.now.to_f
 | 
					      start_at        = Time.now.to_f
 | 
				
			||||||
      seed            = start ? [start] : Account.remote.domains
 | 
					      seed            = start ? [start] : Account.remote.domains
 | 
				
			||||||
 | 
					      blocked_domains = Regexp.new('\\.?' + DomainBlock.where(severity: 1).pluck(:domain).join('|') + '$')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      pool = Concurrent::ThreadPoolExecutor.new(min_threads: 0, max_threads: options[:concurrency], idletime: 10, auto_terminate: true, max_queue: 0)
 | 
					      pool = Concurrent::ThreadPoolExecutor.new(min_threads: 0, max_threads: options[:concurrency], idletime: 10, auto_terminate: true, max_queue: 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      work_unit = ->(domain) do
 | 
					      work_unit = ->(domain) do
 | 
				
			||||||
        next if stats.key?(domain)
 | 
					        next if stats.key?(domain)
 | 
				
			||||||
 | 
					        next if options[:exclude_suspended] && domain.match(blocked_domains)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        stats[domain] = nil
 | 
					        stats[domain] = nil
 | 
				
			||||||
        processed.increment
 | 
					        processed.increment
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -67,7 +67,7 @@
 | 
				
			||||||
    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
 | 
					    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
 | 
				
			||||||
    "@babel/plugin-transform-react-inline-elements": "^7.2.0",
 | 
					    "@babel/plugin-transform-react-inline-elements": "^7.2.0",
 | 
				
			||||||
    "@babel/plugin-transform-react-jsx-self": "^7.2.0",
 | 
					    "@babel/plugin-transform-react-jsx-self": "^7.2.0",
 | 
				
			||||||
    "@babel/plugin-transform-react-jsx-source": "^7.2.0",
 | 
					    "@babel/plugin-transform-react-jsx-source": "^7.5.0",
 | 
				
			||||||
    "@babel/plugin-transform-runtime": "^7.4.4",
 | 
					    "@babel/plugin-transform-runtime": "^7.4.4",
 | 
				
			||||||
    "@babel/preset-env": "^7.5.5",
 | 
					    "@babel/preset-env": "^7.5.5",
 | 
				
			||||||
    "@babel/preset-react": "^7.0.0",
 | 
					    "@babel/preset-react": "^7.0.0",
 | 
				
			||||||
| 
						 | 
					@ -164,7 +164,7 @@
 | 
				
			||||||
    "webpack": "^4.35.3",
 | 
					    "webpack": "^4.35.3",
 | 
				
			||||||
    "webpack-assets-manifest": "^3.1.1",
 | 
					    "webpack-assets-manifest": "^3.1.1",
 | 
				
			||||||
    "webpack-bundle-analyzer": "^3.3.2",
 | 
					    "webpack-bundle-analyzer": "^3.3.2",
 | 
				
			||||||
    "webpack-cli": "^3.3.5",
 | 
					    "webpack-cli": "^3.3.6",
 | 
				
			||||||
    "webpack-merge": "^4.2.1",
 | 
					    "webpack-merge": "^4.2.1",
 | 
				
			||||||
    "websocket.js": "^0.1.12"
 | 
					    "websocket.js": "^0.1.12"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
| 
						 | 
					@ -177,7 +177,7 @@
 | 
				
			||||||
    "eslint-plugin-import": "~2.18.0",
 | 
					    "eslint-plugin-import": "~2.18.0",
 | 
				
			||||||
    "eslint-plugin-jsx-a11y": "~6.2.3",
 | 
					    "eslint-plugin-jsx-a11y": "~6.2.3",
 | 
				
			||||||
    "eslint-plugin-promise": "~4.2.1",
 | 
					    "eslint-plugin-promise": "~4.2.1",
 | 
				
			||||||
    "eslint-plugin-react": "~7.14.2",
 | 
					    "eslint-plugin-react": "~7.14.3",
 | 
				
			||||||
    "jest": "^24.8.0",
 | 
					    "jest": "^24.8.0",
 | 
				
			||||||
    "raf": "^3.4.1",
 | 
					    "raf": "^3.4.1",
 | 
				
			||||||
    "react-intl-translations-manager": "^5.0.3",
 | 
					    "react-intl-translations-manager": "^5.0.3",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,17 +56,6 @@ PEM
 | 
				
			||||||
      expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
 | 
					      expect(json[:aliases]).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it 'returns JSON when account can be found' do
 | 
					 | 
				
			||||||
      get :show, params: { resource: alice.to_webfinger_s }, format: :xml
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      xml = Nokogiri::XML(response.body)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(response).to have_http_status(200)
 | 
					 | 
				
			||||||
      expect(response.content_type).to eq 'application/xrd+xml'
 | 
					 | 
				
			||||||
      expect(xml.at_xpath('//xmlns:Subject').content).to eq 'acct:alice@cb6e6126.ngrok.io'
 | 
					 | 
				
			||||||
      expect(xml.xpath('//xmlns:Alias').map(&:content)).to include('https://cb6e6126.ngrok.io/@alice', 'https://cb6e6126.ngrok.io/users/alice')
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it 'returns http not found when account cannot be found' do
 | 
					    it 'returns http not found when account cannot be found' do
 | 
				
			||||||
      get :show, params: { resource: 'acct:not@existing.com' }, format: :json
 | 
					      get :show, params: { resource: 'acct:not@existing.com' }, format: :json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -254,6 +254,23 @@ RSpec.describe FeedManager do
 | 
				
			||||||
        expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be false
 | 
					        expect(FeedManager.instance.push_to_home(account, reblogs.last)).to be false
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'saves a new reblog of a recently-reblogged status when previous reblog has been deleted' do
 | 
				
			||||||
 | 
					        account = Fabricate(:account)
 | 
				
			||||||
 | 
					        reblogged = Fabricate(:status)
 | 
				
			||||||
 | 
					        old_reblog = Fabricate(:status, reblog: reblogged)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # The first reblog should be accepted
 | 
				
			||||||
 | 
					        expect(FeedManager.instance.push_to_home(account, old_reblog)).to be true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # The first reblog should be successfully removed
 | 
				
			||||||
 | 
					        expect(FeedManager.instance.unpush_from_home(account, old_reblog)).to be true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        reblog = Fabricate(:status, reblog: reblogged)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # The second reblog should be accepted
 | 
				
			||||||
 | 
					        expect(FeedManager.instance.push_to_home(account, reblog)).to be true
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do
 | 
					      it 'does not save a new reblog of a multiply-reblogged-then-unreblogged status' do
 | 
				
			||||||
        account   = Fabricate(:account)
 | 
					        account   = Fabricate(:account)
 | 
				
			||||||
        reblogged = Fabricate(:status)
 | 
					        reblogged = Fabricate(:status)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -136,8 +136,8 @@ RSpec.describe Tag, type: :model do
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it 'finds the exact matching tag as the first item' do
 | 
					    it 'finds the exact matching tag as the first item' do
 | 
				
			||||||
      similar_tag = Fabricate(:tag, name: "matchlater")
 | 
					      similar_tag = Fabricate(:tag, name: "matchlater", score: 1)
 | 
				
			||||||
      tag = Fabricate(:tag, name: "match")
 | 
					      tag = Fabricate(:tag, name: "match", score: 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      results = Tag.search_for("match")
 | 
					      results = Tag.search_for("match")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,23 +12,6 @@ describe 'The webfinger route' do
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe 'asking for xml format' do
 | 
					 | 
				
			||||||
    it 'returns an xml response for xml format' do
 | 
					 | 
				
			||||||
      get webfinger_url(resource: alice.to_webfinger_s, format: :xml)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(response).to have_http_status(200)
 | 
					 | 
				
			||||||
      expect(response.content_type).to eq 'application/xrd+xml'
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    it 'returns an xml response for xml accept header' do
 | 
					 | 
				
			||||||
      headers = { 'HTTP_ACCEPT' => 'application/xrd+xml' }
 | 
					 | 
				
			||||||
      get webfinger_url(resource: alice.to_webfinger_s), headers: headers
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      expect(response).to have_http_status(200)
 | 
					 | 
				
			||||||
      expect(response.content_type).to eq 'application/xrd+xml'
 | 
					 | 
				
			||||||
    end
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  describe 'asking for json format' do
 | 
					  describe 'asking for json format' do
 | 
				
			||||||
    it 'returns a json response for json format' do
 | 
					    it 'returns a json response for json format' do
 | 
				
			||||||
      get webfinger_url(resource: alice.to_webfinger_s, format: :json)
 | 
					      get webfinger_url(resource: alice.to_webfinger_s, format: :json)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										24
									
								
								yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								yarn.lock
									
									
									
									
									
								
							| 
						 | 
					@ -643,10 +643,10 @@
 | 
				
			||||||
    "@babel/helper-plugin-utils" "^7.0.0"
 | 
					    "@babel/helper-plugin-utils" "^7.0.0"
 | 
				
			||||||
    "@babel/plugin-syntax-jsx" "^7.2.0"
 | 
					    "@babel/plugin-syntax-jsx" "^7.2.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"@babel/plugin-transform-react-jsx-source@^7.0.0", "@babel/plugin-transform-react-jsx-source@^7.2.0":
 | 
					"@babel/plugin-transform-react-jsx-source@^7.0.0", "@babel/plugin-transform-react-jsx-source@^7.5.0":
 | 
				
			||||||
  version "7.2.0"
 | 
					  version "7.5.0"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.2.0.tgz#20c8c60f0140f5dd3cd63418d452801cf3f7180f"
 | 
					  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.5.0.tgz#583b10c49cf057e237085bcbd8cc960bd83bd96b"
 | 
				
			||||||
  integrity sha512-A32OkKTp4i5U6aE88GwwcuV4HAprUgHcTq0sSafLxjr6AW0QahrCRCjxogkbbcdtpbXkuTOlgpjophCxb6sh5g==
 | 
					  integrity sha512-58Q+Jsy4IDCZx7kqEZuSDdam/1oW8OdDX8f+Loo6xyxdfg1yF0GE2XNJQSTZCaMol93+FBzpWiPEwtbMloAcPg==
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    "@babel/helper-plugin-utils" "^7.0.0"
 | 
					    "@babel/helper-plugin-utils" "^7.0.0"
 | 
				
			||||||
    "@babel/plugin-syntax-jsx" "^7.2.0"
 | 
					    "@babel/plugin-syntax-jsx" "^7.2.0"
 | 
				
			||||||
| 
						 | 
					@ -3734,10 +3734,10 @@ eslint-plugin-promise@~4.2.1:
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
 | 
					  resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a"
 | 
				
			||||||
  integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==
 | 
					  integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw==
 | 
				
			||||||
 | 
					
 | 
				
			||||||
eslint-plugin-react@~7.14.2:
 | 
					eslint-plugin-react@~7.14.3:
 | 
				
			||||||
  version "7.14.2"
 | 
					  version "7.14.3"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.2.tgz#94c193cc77a899ac0ecbb2766fbef88685b7ecc1"
 | 
					  resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.14.3.tgz#911030dd7e98ba49e1b2208599571846a66bdf13"
 | 
				
			||||||
  integrity sha512-jZdnKe3ip7FQOdjxks9XPN0pjUKZYq48OggNMd16Sk+8VXx6JOvXmlElxROCgp7tiUsTsze3jd78s/9AFJP2mA==
 | 
					  integrity sha512-EzdyyBWC4Uz2hPYBiEJrKCUi2Fn+BJ9B/pJQcjw5X+x/H2Nm59S4MJIvL4O5NEE0+WbnQwEBxWY03oUk+Bc3FA==
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    array-includes "^3.0.3"
 | 
					    array-includes "^3.0.3"
 | 
				
			||||||
    doctrine "^2.1.0"
 | 
					    doctrine "^2.1.0"
 | 
				
			||||||
| 
						 | 
					@ -10337,10 +10337,10 @@ webpack-bundle-analyzer@^3.3.2:
 | 
				
			||||||
    opener "^1.5.1"
 | 
					    opener "^1.5.1"
 | 
				
			||||||
    ws "^6.0.0"
 | 
					    ws "^6.0.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
webpack-cli@^3.3.5:
 | 
					webpack-cli@^3.3.6:
 | 
				
			||||||
  version "3.3.5"
 | 
					  version "3.3.6"
 | 
				
			||||||
  resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.5.tgz#f4d1238a66a2843d9cebf189835ea22142e72767"
 | 
					  resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.6.tgz#2c8c399a2642133f8d736a359007a052e060032c"
 | 
				
			||||||
  integrity sha512-w0j/s42c5UhchwTmV/45MLQnTVwRoaUTu9fM5LuyOd/8lFoCNCELDogFoecx5NzRUndO0yD/gF2b02XKMnmAWQ==
 | 
					  integrity sha512-0vEa83M7kJtxK/jUhlpZ27WHIOndz5mghWL2O53kiDoA9DIxSKnfqB92LoqEn77cT4f3H2cZm1BMEat/6AZz3A==
 | 
				
			||||||
  dependencies:
 | 
					  dependencies:
 | 
				
			||||||
    chalk "2.4.2"
 | 
					    chalk "2.4.2"
 | 
				
			||||||
    cross-spawn "6.0.5"
 | 
					    cross-spawn "6.0.5"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in a new issue