Merge pull request #916 from ThibG/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
		
						commit
						ab9082b332
					
				
							
								
								
									
										2
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										2
									
								
								Gemfile
								
								
								
								
							| 
						 | 
					@ -24,7 +24,7 @@ gem 'streamio-ffmpeg', '~> 3.0'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gem 'active_model_serializers', '~> 0.10'
 | 
					gem 'active_model_serializers', '~> 0.10'
 | 
				
			||||||
gem 'addressable', '~> 2.6'
 | 
					gem 'addressable', '~> 2.6'
 | 
				
			||||||
gem 'bootsnap', '~> 1.3', require: false
 | 
					gem 'bootsnap', '~> 1.4', require: false
 | 
				
			||||||
gem 'browser'
 | 
					gem 'browser'
 | 
				
			||||||
gem 'charlock_holmes', '~> 0.7.6'
 | 
					gem 'charlock_holmes', '~> 0.7.6'
 | 
				
			||||||
gem 'iso-639'
 | 
					gem 'iso-639'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										14
									
								
								Gemfile.lock
								
								
								
								
							
							
						
						
									
										14
									
								
								Gemfile.lock
								
								
								
								
							| 
						 | 
					@ -92,13 +92,13 @@ GEM
 | 
				
			||||||
    aws-sigv4 (1.0.3)
 | 
					    aws-sigv4 (1.0.3)
 | 
				
			||||||
    bcrypt (3.1.12)
 | 
					    bcrypt (3.1.12)
 | 
				
			||||||
    benchmark-ips (2.7.2)
 | 
					    benchmark-ips (2.7.2)
 | 
				
			||||||
    better_errors (2.5.0)
 | 
					    better_errors (2.5.1)
 | 
				
			||||||
      coderay (>= 1.0.0)
 | 
					      coderay (>= 1.0.0)
 | 
				
			||||||
      erubi (>= 1.0.0)
 | 
					      erubi (>= 1.0.0)
 | 
				
			||||||
      rack (>= 0.9.0)
 | 
					      rack (>= 0.9.0)
 | 
				
			||||||
    binding_of_caller (0.8.0)
 | 
					    binding_of_caller (0.8.0)
 | 
				
			||||||
      debug_inspector (>= 0.0.1)
 | 
					      debug_inspector (>= 0.0.1)
 | 
				
			||||||
    bootsnap (1.3.2)
 | 
					    bootsnap (1.4.0)
 | 
				
			||||||
      msgpack (~> 1.0)
 | 
					      msgpack (~> 1.0)
 | 
				
			||||||
    brakeman (4.4.0)
 | 
					    brakeman (4.4.0)
 | 
				
			||||||
    browser (2.5.3)
 | 
					    browser (2.5.3)
 | 
				
			||||||
| 
						 | 
					@ -205,7 +205,7 @@ GEM
 | 
				
			||||||
      tzinfo
 | 
					      tzinfo
 | 
				
			||||||
    excon (0.62.0)
 | 
					    excon (0.62.0)
 | 
				
			||||||
    fabrication (2.20.1)
 | 
					    fabrication (2.20.1)
 | 
				
			||||||
    faker (1.9.1)
 | 
					    faker (1.9.3)
 | 
				
			||||||
      i18n (>= 0.7)
 | 
					      i18n (>= 0.7)
 | 
				
			||||||
    faraday (0.15.0)
 | 
					    faraday (0.15.0)
 | 
				
			||||||
      multipart-post (>= 1.2, < 3)
 | 
					      multipart-post (>= 1.2, < 3)
 | 
				
			||||||
| 
						 | 
					@ -347,7 +347,7 @@ GEM
 | 
				
			||||||
    mini_mime (1.0.1)
 | 
					    mini_mime (1.0.1)
 | 
				
			||||||
    mini_portile2 (2.4.0)
 | 
					    mini_portile2 (2.4.0)
 | 
				
			||||||
    minitest (5.11.3)
 | 
					    minitest (5.11.3)
 | 
				
			||||||
    msgpack (1.2.4)
 | 
					    msgpack (1.2.6)
 | 
				
			||||||
    multi_json (1.13.1)
 | 
					    multi_json (1.13.1)
 | 
				
			||||||
    multipart-post (2.0.0)
 | 
					    multipart-post (2.0.0)
 | 
				
			||||||
    necromancer (0.4.0)
 | 
					    necromancer (0.4.0)
 | 
				
			||||||
| 
						 | 
					@ -402,7 +402,7 @@ GEM
 | 
				
			||||||
    pg (1.1.4)
 | 
					    pg (1.1.4)
 | 
				
			||||||
    pghero (2.2.0)
 | 
					    pghero (2.2.0)
 | 
				
			||||||
      activerecord
 | 
					      activerecord
 | 
				
			||||||
    pkg-config (1.3.2)
 | 
					    pkg-config (1.3.3)
 | 
				
			||||||
    powerpack (0.1.2)
 | 
					    powerpack (0.1.2)
 | 
				
			||||||
    premailer (1.11.1)
 | 
					    premailer (1.11.1)
 | 
				
			||||||
      addressable
 | 
					      addressable
 | 
				
			||||||
| 
						 | 
					@ -565,7 +565,7 @@ GEM
 | 
				
			||||||
      rufus-scheduler (~> 3.2)
 | 
					      rufus-scheduler (~> 3.2)
 | 
				
			||||||
      sidekiq (>= 3)
 | 
					      sidekiq (>= 3)
 | 
				
			||||||
      tilt (>= 1.4.0)
 | 
					      tilt (>= 1.4.0)
 | 
				
			||||||
    sidekiq-unique-jobs (6.0.8)
 | 
					    sidekiq-unique-jobs (6.0.9)
 | 
				
			||||||
      concurrent-ruby (~> 1.0, >= 1.0.5)
 | 
					      concurrent-ruby (~> 1.0, >= 1.0.5)
 | 
				
			||||||
      sidekiq (>= 4.0, < 6.0)
 | 
					      sidekiq (>= 4.0, < 6.0)
 | 
				
			||||||
      thor (~> 0)
 | 
					      thor (~> 0)
 | 
				
			||||||
| 
						 | 
					@ -662,7 +662,7 @@ DEPENDENCIES
 | 
				
			||||||
  aws-sdk-s3 (~> 1.30)
 | 
					  aws-sdk-s3 (~> 1.30)
 | 
				
			||||||
  better_errors (~> 2.5)
 | 
					  better_errors (~> 2.5)
 | 
				
			||||||
  binding_of_caller (~> 0.7)
 | 
					  binding_of_caller (~> 0.7)
 | 
				
			||||||
  bootsnap (~> 1.3)
 | 
					  bootsnap (~> 1.4)
 | 
				
			||||||
  brakeman (~> 4.4)
 | 
					  brakeman (~> 4.4)
 | 
				
			||||||
  browser
 | 
					  browser
 | 
				
			||||||
  bullet (~> 5.9)
 | 
					  bullet (~> 5.9)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,7 +31,7 @@ class StatusesIndex < Chewy::Index
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  define_type ::Status.unscoped.without_reblogs do
 | 
					  define_type ::Status.unscoped.without_reblogs.includes(:media_attachments) do
 | 
				
			||||||
    crutch :mentions do |collection|
 | 
					    crutch :mentions do |collection|
 | 
				
			||||||
      data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
 | 
					      data = ::Mention.where(status_id: collection.map(&:id)).pluck(:status_id, :account_id)
 | 
				
			||||||
      data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
 | 
					      data.each.with_object({}) { |(id, name), result| (result[id] ||= []).push(name) }
 | 
				
			||||||
| 
						 | 
					@ -50,7 +50,7 @@ class StatusesIndex < Chewy::Index
 | 
				
			||||||
    root date_detection: false do
 | 
					    root date_detection: false do
 | 
				
			||||||
      field :account_id, type: 'long'
 | 
					      field :account_id, type: 'long'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].join("\n\n") } do
 | 
					      field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).join("\n\n") } do
 | 
				
			||||||
        field :stemmed, type: 'text', analyzer: 'content'
 | 
					        field :stemmed, type: 'text', analyzer: 'content'
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
 | 
				
			||||||
    resource.invite_code = params[:invite_code] if resource.invite_code.blank?
 | 
					    resource.invite_code = params[:invite_code] if resource.invite_code.blank?
 | 
				
			||||||
    resource.agreement   = true
 | 
					    resource.agreement   = true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    resource.current_sign_in_ip = request.remote_ip if resource.current_sign_in_ip.nil?
 | 
				
			||||||
    resource.build_account if resource.account.nil?
 | 
					    resource.build_account if resource.account.nil?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,7 +4,7 @@ module SettingsHelper
 | 
				
			||||||
  HUMAN_LOCALES = {
 | 
					  HUMAN_LOCALES = {
 | 
				
			||||||
    en: 'English',
 | 
					    en: 'English',
 | 
				
			||||||
    ar: 'العربية',
 | 
					    ar: 'العربية',
 | 
				
			||||||
    ast: 'l\'asturianu',
 | 
					    ast: 'Asturianu',
 | 
				
			||||||
    bg: 'Български',
 | 
					    bg: 'Български',
 | 
				
			||||||
    ca: 'Català',
 | 
					    ca: 'Català',
 | 
				
			||||||
    co: 'Corsu',
 | 
					    co: 'Corsu',
 | 
				
			||||||
| 
						 | 
					@ -30,16 +30,16 @@ module SettingsHelper
 | 
				
			||||||
    ja: '日本語',
 | 
					    ja: '日本語',
 | 
				
			||||||
    ka: 'ქართული',
 | 
					    ka: 'ქართული',
 | 
				
			||||||
    ko: '한국어',
 | 
					    ko: '한국어',
 | 
				
			||||||
    lv: 'Latviešu valoda',
 | 
					    lv: 'Latviešu',
 | 
				
			||||||
    ml: 'മലയാളം',
 | 
					    ml: 'മലയാളം',
 | 
				
			||||||
    ms: 'بهاس ملايو',
 | 
					    ms: 'Bahasa Melayu',
 | 
				
			||||||
    nl: 'Nederlands',
 | 
					    nl: 'Nederlands',
 | 
				
			||||||
    no: 'Norsk',
 | 
					    no: 'Norsk',
 | 
				
			||||||
    oc: 'Occitan',
 | 
					    oc: 'Occitan',
 | 
				
			||||||
    pl: 'Polszczyzna',
 | 
					    pl: 'Polski',
 | 
				
			||||||
    pt: 'Português',
 | 
					    pt: 'Português',
 | 
				
			||||||
    'pt-BR': 'Português do Brasil',
 | 
					    'pt-BR': 'Português do Brasil',
 | 
				
			||||||
    ro: 'Limba română',
 | 
					    ro: 'Română',
 | 
				
			||||||
    ru: 'Русский',
 | 
					    ru: 'Русский',
 | 
				
			||||||
    sk: 'Slovenčina',
 | 
					    sk: 'Slovenčina',
 | 
				
			||||||
    sl: 'Slovenščina',
 | 
					    sl: 'Slovenščina',
 | 
				
			||||||
| 
						 | 
					@ -49,7 +49,7 @@ module SettingsHelper
 | 
				
			||||||
    sv: 'Svenska',
 | 
					    sv: 'Svenska',
 | 
				
			||||||
    ta: 'தமிழ்',
 | 
					    ta: 'தமிழ்',
 | 
				
			||||||
    te: 'తెలుగు',
 | 
					    te: 'తెలుగు',
 | 
				
			||||||
    th: 'ภาษาไทย',
 | 
					    th: 'ไทย',
 | 
				
			||||||
    tr: 'Türkçe',
 | 
					    tr: 'Türkçe',
 | 
				
			||||||
    uk: 'Українська',
 | 
					    uk: 'Українська',
 | 
				
			||||||
    zh: '中文',
 | 
					    zh: '中文',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,15 @@
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
import { injectIntl, FormattedMessage } from 'react-intl';
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
import Toggle from 'react-toggle';
 | 
					import Toggle from 'react-toggle';
 | 
				
			||||||
import AsyncSelect from 'react-select/lib/Async';
 | 
					import AsyncSelect from 'react-select/lib/Async';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
 | 
				
			||||||
 | 
					  noOptions: { id: 'hashtag.column_settings.select.no_options_message', defaultMessage: 'No suggestions found' },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@injectIntl
 | 
					@injectIntl
 | 
				
			||||||
export default class ColumnSettings extends React.PureComponent {
 | 
					export default class ColumnSettings extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +30,7 @@ export default class ColumnSettings extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  tags (mode) {
 | 
					  tags (mode) {
 | 
				
			||||||
    let tags = this.props.settings.getIn(['tags', mode]) || [];
 | 
					    let tags = this.props.settings.getIn(['tags', mode]) || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (tags.toJSON) {
 | 
					    if (tags.toJSON) {
 | 
				
			||||||
      return tags.toJSON();
 | 
					      return tags.toJSON();
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
| 
						 | 
					@ -32,33 +38,36 @@ export default class ColumnSettings extends React.PureComponent {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onSelect = (mode) => {
 | 
					  onSelect = mode => value => this.props.onChange(['tags', mode], value);
 | 
				
			||||||
    return (value) => {
 | 
					 | 
				
			||||||
      this.props.onChange(['tags', mode], value);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onToggle = () => {
 | 
					  onToggle = () => {
 | 
				
			||||||
    if (this.state.open && this.hasTags()) {
 | 
					    if (this.state.open && this.hasTags()) {
 | 
				
			||||||
      this.props.onChange('tags', {});
 | 
					      this.props.onChange('tags', {});
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.setState({ open: !this.state.open });
 | 
					    this.setState({ open: !this.state.open });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  noOptionsMessage = () => this.props.intl.formatMessage(messages.noOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  modeSelect (mode) {
 | 
					  modeSelect (mode) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className='column-settings__section'>
 | 
					      <div className='column-settings__row'>
 | 
				
			||||||
        {this.modeLabel(mode)}
 | 
					        <span className='column-settings__section'>
 | 
				
			||||||
 | 
					          {this.modeLabel(mode)}
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <AsyncSelect
 | 
					        <AsyncSelect
 | 
				
			||||||
          isMulti
 | 
					          isMulti
 | 
				
			||||||
          autoFocus
 | 
					          autoFocus
 | 
				
			||||||
          value={this.tags(mode)}
 | 
					          value={this.tags(mode)}
 | 
				
			||||||
          settings={this.props.settings}
 | 
					 | 
				
			||||||
          settingPath={['tags', mode]}
 | 
					 | 
				
			||||||
          onChange={this.onSelect(mode)}
 | 
					          onChange={this.onSelect(mode)}
 | 
				
			||||||
          loadOptions={this.props.onLoad}
 | 
					          loadOptions={this.props.onLoad}
 | 
				
			||||||
          classNamePrefix='column-settings__hashtag-select'
 | 
					          className='column-select__container'
 | 
				
			||||||
 | 
					          classNamePrefix='column-select'
 | 
				
			||||||
          name='tags'
 | 
					          name='tags'
 | 
				
			||||||
 | 
					          placeholder={this.props.intl.formatMessage(messages.placeholder)}
 | 
				
			||||||
 | 
					          noOptionsMessage={this.noOptionsMessage}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					@ -66,11 +75,15 @@ export default class ColumnSettings extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  modeLabel (mode) {
 | 
					  modeLabel (mode) {
 | 
				
			||||||
    switch(mode) {
 | 
					    switch(mode) {
 | 
				
			||||||
    case 'any':  return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
 | 
					    case 'any':
 | 
				
			||||||
    case 'all':  return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
 | 
					      return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
 | 
				
			||||||
    case 'none': return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
 | 
					    case 'all':
 | 
				
			||||||
 | 
					      return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
 | 
				
			||||||
 | 
					    case 'none':
 | 
				
			||||||
 | 
					      return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return '';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return '';
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
| 
						 | 
					@ -78,23 +91,21 @@ export default class ColumnSettings extends React.PureComponent {
 | 
				
			||||||
      <div>
 | 
					      <div>
 | 
				
			||||||
        <div className='column-settings__row'>
 | 
					        <div className='column-settings__row'>
 | 
				
			||||||
          <div className='setting-toggle'>
 | 
					          <div className='setting-toggle'>
 | 
				
			||||||
            <Toggle
 | 
					            <Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
 | 
				
			||||||
              id='hashtag.column_settings.tag_toggle'
 | 
					
 | 
				
			||||||
              onChange={this.onToggle}
 | 
					 | 
				
			||||||
              checked={this.state.open}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <span className='setting-toggle__label'>
 | 
					            <span className='setting-toggle__label'>
 | 
				
			||||||
              <FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
 | 
					              <FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
 | 
				
			||||||
            </span>
 | 
					            </span>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        {this.state.open &&
 | 
					
 | 
				
			||||||
 | 
					        {this.state.open && (
 | 
				
			||||||
          <div className='column-settings__hashtags'>
 | 
					          <div className='column-settings__hashtags'>
 | 
				
			||||||
            {this.modeSelect('any')}
 | 
					            {this.modeSelect('any')}
 | 
				
			||||||
            {this.modeSelect('all')}
 | 
					            {this.modeSelect('all')}
 | 
				
			||||||
            {this.modeSelect('none')}
 | 
					            {this.modeSelect('none')}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        }
 | 
					        )}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -82,3 +82,34 @@
 | 
				
			||||||
    font-size: 16px;
 | 
					    font-size: 16px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@mixin search-popout() {
 | 
				
			||||||
 | 
					  background: $simple-background-color;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  padding: 10px 14px;
 | 
				
			||||||
 | 
					  padding-bottom: 14px;
 | 
				
			||||||
 | 
					  margin-top: 10px;
 | 
				
			||||||
 | 
					  color: $light-text-color;
 | 
				
			||||||
 | 
					  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  h4 {
 | 
				
			||||||
 | 
					    text-transform: uppercase;
 | 
				
			||||||
 | 
					    color: $light-text-color;
 | 
				
			||||||
 | 
					    font-size: 13px;
 | 
				
			||||||
 | 
					    font-weight: 500;
 | 
				
			||||||
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  li {
 | 
				
			||||||
 | 
					    padding: 4px 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ul {
 | 
				
			||||||
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  em {
 | 
				
			||||||
 | 
					    font-weight: 500;
 | 
				
			||||||
 | 
					    color: $inverted-text-color;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,15 +49,9 @@ $small-breakpoint: 960px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  strong,
 | 
				
			||||||
  em {
 | 
					  em {
 | 
				
			||||||
    display: inline;
 | 
					 | 
				
			||||||
    margin: 0;
 | 
					 | 
				
			||||||
    padding: 0;
 | 
					 | 
				
			||||||
    font-weight: 700;
 | 
					    font-weight: 700;
 | 
				
			||||||
    background: transparent;
 | 
					 | 
				
			||||||
    font-family: inherit;
 | 
					 | 
				
			||||||
    font-size: inherit;
 | 
					 | 
				
			||||||
    line-height: inherit;
 | 
					 | 
				
			||||||
    color: lighten($darker-text-color, 10%);
 | 
					    color: lighten($darker-text-color, 10%);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -798,7 +792,7 @@ $small-breakpoint: 960px;
 | 
				
			||||||
      width: 100%;
 | 
					      width: 100%;
 | 
				
			||||||
      display: flex;
 | 
					      display: flex;
 | 
				
			||||||
      flex-direction: row-reverse;
 | 
					      flex-direction: row-reverse;
 | 
				
			||||||
      flex-wrap: wrap;
 | 
					      flex-wrap: nowrap;
 | 
				
			||||||
      justify-content: space-between;
 | 
					      justify-content: space-between;
 | 
				
			||||||
      align-items: center;
 | 
					      align-items: center;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -848,14 +842,7 @@ $small-breakpoint: 960px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    strong {
 | 
					    strong {
 | 
				
			||||||
      display: inline;
 | 
					      font-weight: 500;
 | 
				
			||||||
      margin: 0;
 | 
					 | 
				
			||||||
      padding: 0;
 | 
					 | 
				
			||||||
      font-weight: 700;
 | 
					 | 
				
			||||||
      background: transparent;
 | 
					 | 
				
			||||||
      font-family: inherit;
 | 
					 | 
				
			||||||
      font-size: inherit;
 | 
					 | 
				
			||||||
      line-height: inherit;
 | 
					 | 
				
			||||||
      color: lighten($darker-text-color, 10%);
 | 
					      color: lighten($darker-text-color, 10%);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -339,14 +339,41 @@
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  font-weight: 500;
 | 
					  font-weight: 500;
 | 
				
			||||||
  margin-bottom: 10px;
 | 
					  margin-bottom: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .column-settings__hashtag-select {
 | 
					.column-settings__hashtags {
 | 
				
			||||||
 | 
					  .column-settings__row {
 | 
				
			||||||
 | 
					    margin-bottom: 15px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .column-select {
 | 
				
			||||||
    &__control {
 | 
					    &__control {
 | 
				
			||||||
      @include search-input();
 | 
					      @include search-input();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__placeholder {
 | 
				
			||||||
 | 
					      color: $dark-text-color;
 | 
				
			||||||
 | 
					      padding-left: 2px;
 | 
				
			||||||
 | 
					      font-size: 12px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__value-container {
 | 
				
			||||||
 | 
					      padding-left: 6px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &__multi-value {
 | 
					    &__multi-value {
 | 
				
			||||||
      background: lighten($ui-base-color, 8%);
 | 
					      background: lighten($ui-base-color, 8%);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &__remove {
 | 
				
			||||||
 | 
					        cursor: pointer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover,
 | 
				
			||||||
 | 
					        &:active,
 | 
				
			||||||
 | 
					        &:focus {
 | 
				
			||||||
 | 
					          background: lighten($ui-base-color, 12%);
 | 
				
			||||||
 | 
					          color: lighten($darker-text-color, 4%);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &__multi-value__label,
 | 
					    &__multi-value__label,
 | 
				
			||||||
| 
						 | 
					@ -354,9 +381,42 @@
 | 
				
			||||||
      color: $darker-text-color;
 | 
					      color: $darker-text-color;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &__indicator-separator,
 | 
					    &__clear-indicator,
 | 
				
			||||||
    &__dropdown-indicator {
 | 
					    &__dropdown-indicator {
 | 
				
			||||||
      display: none;
 | 
					      cursor: pointer;
 | 
				
			||||||
 | 
					      transition: none;
 | 
				
			||||||
 | 
					      color: $dark-text-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:hover,
 | 
				
			||||||
 | 
					      &:active,
 | 
				
			||||||
 | 
					      &:focus {
 | 
				
			||||||
 | 
					        color: lighten($dark-text-color, 4%);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__indicator-separator {
 | 
				
			||||||
 | 
					      background-color: lighten($ui-base-color, 8%);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__menu {
 | 
				
			||||||
 | 
					      @include search-popout();
 | 
				
			||||||
 | 
					      padding: 0;
 | 
				
			||||||
 | 
					      background: $ui-secondary-color;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__menu-list {
 | 
				
			||||||
 | 
					      padding: 6px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__option {
 | 
				
			||||||
 | 
					      color: $inverted-text-color;
 | 
				
			||||||
 | 
					      border-radius: 4px;
 | 
				
			||||||
 | 
					      font-size: 14px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &--is-focused,
 | 
				
			||||||
 | 
					      &--is-selected {
 | 
				
			||||||
 | 
					        background: darken($ui-secondary-color, 10%);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -157,29 +157,7 @@
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.drawer--search--popout {
 | 
					.drawer--search--popout {
 | 
				
			||||||
  box-sizing: border-box;
 | 
					  @include search-popout();
 | 
				
			||||||
  margin-top: 10px;
 | 
					 | 
				
			||||||
  border-radius: 4px;
 | 
					 | 
				
			||||||
  padding: 10px 14px 14px 14px;
 | 
					 | 
				
			||||||
  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
 | 
					 | 
				
			||||||
  color: $light-text-color;
 | 
					 | 
				
			||||||
  background: $simple-background-color;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  h4 {
 | 
					 | 
				
			||||||
    margin-bottom: 10px;
 | 
					 | 
				
			||||||
    color: $light-text-color;
 | 
					 | 
				
			||||||
    font-size: 13px;
 | 
					 | 
				
			||||||
    font-weight: 500;
 | 
					 | 
				
			||||||
    text-transform: uppercase;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ul { margin-bottom: 10px }
 | 
					 | 
				
			||||||
  li { padding: 4px 0 }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  em {
 | 
					 | 
				
			||||||
    color: $inverted-text-color;
 | 
					 | 
				
			||||||
    font-weight: 500;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.drawer--account {
 | 
					.drawer--account {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,10 @@
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.rich-formatting a,
 | 
				
			||||||
 | 
					.rich-formatting p a,
 | 
				
			||||||
 | 
					.rich-formatting li a,
 | 
				
			||||||
 | 
					.landing-page__short-description p a,
 | 
				
			||||||
.status__content a,
 | 
					.status__content a,
 | 
				
			||||||
.reply-indicator__content a {
 | 
					.reply-indicator__content a {
 | 
				
			||||||
  color: lighten($ui-highlight-color, 12%);
 | 
					  color: lighten($ui-highlight-color, 12%);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,26 +11,36 @@ export default class DisplayName extends React.PureComponent {
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { account, others, localDomain } = this.props;
 | 
					    const { others, localDomain } = this.props;
 | 
				
			||||||
    const displayNameHtml = { __html: account.get('display_name_html') };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let suffix;
 | 
					    let displayName, suffix, account;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (others && others.size > 1) {
 | 
					    if (others && others.size > 1) {
 | 
				
			||||||
      suffix = `+${others.size}`;
 | 
					      displayName = others.take(2).map(a => <bdi key={a.get('id')}><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi>).reduce((prev, cur) => [prev, ', ', cur]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (others.size - 2 > 0) {
 | 
				
			||||||
 | 
					        suffix = `+${others.size - 2}`;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
 | 
					      if (others) {
 | 
				
			||||||
 | 
					        account = others.first();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        account = this.props.account;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      let acct = account.get('acct');
 | 
					      let acct = account.get('acct');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (acct.indexOf('@') === -1 && localDomain) {
 | 
					      if (acct.indexOf('@') === -1 && localDomain) {
 | 
				
			||||||
        acct = `${acct}@${localDomain}`;
 | 
					        acct = `${acct}@${localDomain}`;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      suffix = <span className='display-name__account'>@{acct}</span>;
 | 
					      displayName = <bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>;
 | 
				
			||||||
 | 
					      suffix      = <span className='display-name__account'>@{acct}</span>;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <span className='display-name'>
 | 
					      <span className='display-name'>
 | 
				
			||||||
        <bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix}
 | 
					        {displayName} {suffix}
 | 
				
			||||||
      </span>
 | 
					      </span>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -86,7 +86,7 @@ class Status extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Track height changes we know about to compensate scrolling
 | 
					  // Track height changes we know about to compensate scrolling
 | 
				
			||||||
  componentDidMount () {
 | 
					  componentDidMount () {
 | 
				
			||||||
    this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status.get('card');
 | 
					    this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getSnapshotBeforeUpdate () {
 | 
					  getSnapshotBeforeUpdate () {
 | 
				
			||||||
| 
						 | 
					@ -99,7 +99,7 @@ class Status extends ImmutablePureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Compensate height changes
 | 
					  // Compensate height changes
 | 
				
			||||||
  componentDidUpdate (prevProps, prevState, snapshot) {
 | 
					  componentDidUpdate (prevProps, prevState, snapshot) {
 | 
				
			||||||
    const doShowCard  = !this.props.muted && !this.props.hidden && this.props.status.get('card');
 | 
					    const doShowCard  = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
 | 
				
			||||||
    if (doShowCard && !this.didShowCard) {
 | 
					    if (doShowCard && !this.didShowCard) {
 | 
				
			||||||
      this.didShowCard = true;
 | 
					      this.didShowCard = true;
 | 
				
			||||||
      if (snapshot !== null && this.props.updateScrollBottom) {
 | 
					      if (snapshot !== null && this.props.updateScrollBottom) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -108,9 +108,8 @@ class Upload extends ImmutablePureComponent {
 | 
				
			||||||
                <label>
 | 
					                <label>
 | 
				
			||||||
                  <span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span>
 | 
					                  <span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                  <input
 | 
					                  <textarea
 | 
				
			||||||
                    placeholder={intl.formatMessage(messages.description)}
 | 
					                    placeholder={intl.formatMessage(messages.description)}
 | 
				
			||||||
                    type='text'
 | 
					 | 
				
			||||||
                    value={description}
 | 
					                    value={description}
 | 
				
			||||||
                    maxLength={420}
 | 
					                    maxLength={420}
 | 
				
			||||||
                    onFocus={this.handleInputFocus}
 | 
					                    onFocus={this.handleInputFocus}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,10 +1,15 @@
 | 
				
			||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import PropTypes from 'prop-types';
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
					import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
				
			||||||
import { injectIntl, FormattedMessage } from 'react-intl';
 | 
					import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
 | 
				
			||||||
import Toggle from 'react-toggle';
 | 
					import Toggle from 'react-toggle';
 | 
				
			||||||
import AsyncSelect from 'react-select/lib/Async';
 | 
					import AsyncSelect from 'react-select/lib/Async';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  placeholder: { id: 'hashtag.column_settings.select.placeholder', defaultMessage: 'Enter hashtags…' },
 | 
				
			||||||
 | 
					  noOptions: { id: 'hashtag.column_settings.select.no_options_message', defaultMessage: 'No suggestions found' },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default @injectIntl
 | 
					export default @injectIntl
 | 
				
			||||||
class ColumnSettings extends React.PureComponent {
 | 
					class ColumnSettings extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +30,7 @@ class ColumnSettings extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  tags (mode) {
 | 
					  tags (mode) {
 | 
				
			||||||
    let tags = this.props.settings.getIn(['tags', mode]) || [];
 | 
					    let tags = this.props.settings.getIn(['tags', mode]) || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (tags.toJSON) {
 | 
					    if (tags.toJSON) {
 | 
				
			||||||
      return tags.toJSON();
 | 
					      return tags.toJSON();
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
| 
						 | 
					@ -32,33 +38,36 @@ class ColumnSettings extends React.PureComponent {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onSelect = (mode) => {
 | 
					  onSelect = mode => value => this.props.onChange(['tags', mode], value);
 | 
				
			||||||
    return (value) => {
 | 
					 | 
				
			||||||
      this.props.onChange(['tags', mode], value);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onToggle = () => {
 | 
					  onToggle = () => {
 | 
				
			||||||
    if (this.state.open && this.hasTags()) {
 | 
					    if (this.state.open && this.hasTags()) {
 | 
				
			||||||
      this.props.onChange('tags', {});
 | 
					      this.props.onChange('tags', {});
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.setState({ open: !this.state.open });
 | 
					    this.setState({ open: !this.state.open });
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  noOptionsMessage = () => this.props.intl.formatMessage(messages.noOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  modeSelect (mode) {
 | 
					  modeSelect (mode) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className='column-settings__section'>
 | 
					      <div className='column-settings__row'>
 | 
				
			||||||
        {this.modeLabel(mode)}
 | 
					        <span className='column-settings__section'>
 | 
				
			||||||
 | 
					          {this.modeLabel(mode)}
 | 
				
			||||||
 | 
					        </span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <AsyncSelect
 | 
					        <AsyncSelect
 | 
				
			||||||
          isMulti
 | 
					          isMulti
 | 
				
			||||||
          autoFocus
 | 
					          autoFocus
 | 
				
			||||||
          value={this.tags(mode)}
 | 
					          value={this.tags(mode)}
 | 
				
			||||||
          settings={this.props.settings}
 | 
					 | 
				
			||||||
          settingPath={['tags', mode]}
 | 
					 | 
				
			||||||
          onChange={this.onSelect(mode)}
 | 
					          onChange={this.onSelect(mode)}
 | 
				
			||||||
          loadOptions={this.props.onLoad}
 | 
					          loadOptions={this.props.onLoad}
 | 
				
			||||||
          classNamePrefix='column-settings__hashtag-select'
 | 
					          className='column-select__container'
 | 
				
			||||||
 | 
					          classNamePrefix='column-select'
 | 
				
			||||||
          name='tags'
 | 
					          name='tags'
 | 
				
			||||||
 | 
					          placeholder={this.props.intl.formatMessage(messages.placeholder)}
 | 
				
			||||||
 | 
					          noOptionsMessage={this.noOptionsMessage}
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
| 
						 | 
					@ -66,11 +75,15 @@ class ColumnSettings extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  modeLabel (mode) {
 | 
					  modeLabel (mode) {
 | 
				
			||||||
    switch(mode) {
 | 
					    switch(mode) {
 | 
				
			||||||
    case 'any':  return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
 | 
					    case 'any':
 | 
				
			||||||
    case 'all':  return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
 | 
					      return <FormattedMessage id='hashtag.column_settings.tag_mode.any' defaultMessage='Any of these' />;
 | 
				
			||||||
    case 'none': return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
 | 
					    case 'all':
 | 
				
			||||||
 | 
					      return <FormattedMessage id='hashtag.column_settings.tag_mode.all' defaultMessage='All of these' />;
 | 
				
			||||||
 | 
					    case 'none':
 | 
				
			||||||
 | 
					      return <FormattedMessage id='hashtag.column_settings.tag_mode.none' defaultMessage='None of these' />;
 | 
				
			||||||
 | 
					    default:
 | 
				
			||||||
 | 
					      return '';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return '';
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
| 
						 | 
					@ -78,23 +91,21 @@ class ColumnSettings extends React.PureComponent {
 | 
				
			||||||
      <div>
 | 
					      <div>
 | 
				
			||||||
        <div className='column-settings__row'>
 | 
					        <div className='column-settings__row'>
 | 
				
			||||||
          <div className='setting-toggle'>
 | 
					          <div className='setting-toggle'>
 | 
				
			||||||
            <Toggle
 | 
					            <Toggle id='hashtag.column_settings.tag_toggle' onChange={this.onToggle} checked={this.state.open} />
 | 
				
			||||||
              id='hashtag.column_settings.tag_toggle'
 | 
					
 | 
				
			||||||
              onChange={this.onToggle}
 | 
					 | 
				
			||||||
              checked={this.state.open}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <span className='setting-toggle__label'>
 | 
					            <span className='setting-toggle__label'>
 | 
				
			||||||
              <FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
 | 
					              <FormattedMessage id='hashtag.column_settings.tag_toggle' defaultMessage='Include additional tags in this column' />
 | 
				
			||||||
            </span>
 | 
					            </span>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        {this.state.open &&
 | 
					
 | 
				
			||||||
 | 
					        {this.state.open && (
 | 
				
			||||||
          <div className='column-settings__hashtags'>
 | 
					          <div className='column-settings__hashtags'>
 | 
				
			||||||
            {this.modeSelect('any')}
 | 
					            {this.modeSelect('any')}
 | 
				
			||||||
            {this.modeSelect('all')}
 | 
					            {this.modeSelect('all')}
 | 
				
			||||||
            {this.modeSelect('none')}
 | 
					            {this.modeSelect('none')}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        }
 | 
					        )}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,15 +41,19 @@ class HashtagTimeline extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  title = () => {
 | 
					  title = () => {
 | 
				
			||||||
    let title = [this.props.params.id];
 | 
					    let title = [this.props.params.id];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.additionalFor('any')) {
 | 
					    if (this.additionalFor('any')) {
 | 
				
			||||||
      title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.any'  values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
 | 
					      title.push(' ', <FormattedMessage key='any' id='hashtag.column_header.tag_mode.any'  values={{ additional: this.additionalFor('any') }} defaultMessage='or {additional}' />);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.additionalFor('all')) {
 | 
					    if (this.additionalFor('all')) {
 | 
				
			||||||
      title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.all'  values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />);
 | 
					      title.push(' ', <FormattedMessage key='all' id='hashtag.column_header.tag_mode.all'  values={{ additional: this.additionalFor('all') }} defaultMessage='and {additional}' />);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.additionalFor('none')) {
 | 
					    if (this.additionalFor('none')) {
 | 
				
			||||||
      title.push(' ', <FormattedMessage id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />);
 | 
					      title.push(' ', <FormattedMessage key='none' id='hashtag.column_header.tag_mode.none' values={{ additional: this.additionalFor('none') }} defaultMessage='without {additional}' />);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return title;
 | 
					    return title;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -77,9 +81,10 @@ class HashtagTimeline extends React.PureComponent {
 | 
				
			||||||
    let all  = (tags.all || []).map(tag => tag.value);
 | 
					    let all  = (tags.all || []).map(tag => tag.value);
 | 
				
			||||||
    let none = (tags.none || []).map(tag => tag.value);
 | 
					    let none = (tags.none || []).map(tag => tag.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [id, ...any].map((tag) => {
 | 
					    [id, ...any].map(tag => {
 | 
				
			||||||
      this.disconnects.push(dispatch(connectHashtagStream(id, tag, (status) => {
 | 
					      this.disconnects.push(dispatch(connectHashtagStream(id, tag, status => {
 | 
				
			||||||
        let tags = status.tags.map(tag => tag.name);
 | 
					        let tags = status.tags.map(tag => tag.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return all.filter(tag => tags.includes(tag)).length === all.length &&
 | 
					        return all.filter(tag => tags.includes(tag)).length === all.length &&
 | 
				
			||||||
               none.filter(tag => tags.includes(tag)).length === 0;
 | 
					               none.filter(tag => tags.includes(tag)).length === 0;
 | 
				
			||||||
      })));
 | 
					      })));
 | 
				
			||||||
| 
						 | 
					@ -95,12 +100,14 @@ class HashtagTimeline extends React.PureComponent {
 | 
				
			||||||
    const { dispatch } = this.props;
 | 
					    const { dispatch } = this.props;
 | 
				
			||||||
    const { id, tags } = this.props.params;
 | 
					    const { id, tags } = this.props.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this._subscribe(dispatch, id, tags);
 | 
				
			||||||
    dispatch(expandHashtagTimeline(id, { tags }));
 | 
					    dispatch(expandHashtagTimeline(id, { tags }));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  componentWillReceiveProps (nextProps) {
 | 
					  componentWillReceiveProps (nextProps) {
 | 
				
			||||||
    const { dispatch, params } = this.props;
 | 
					    const { dispatch, params } = this.props;
 | 
				
			||||||
    const { id, tags } = nextProps.params;
 | 
					    const { id, tags } = nextProps.params;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (id !== params.id || !isEqual(tags, params.tags)) {
 | 
					    if (id !== params.id || !isEqual(tags, params.tags)) {
 | 
				
			||||||
      this._unsubscribe();
 | 
					      this._unsubscribe();
 | 
				
			||||||
      this._subscribe(dispatch, id, tags);
 | 
					      this._subscribe(dispatch, id, tags);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,70 @@
 | 
				
			||||||
 | 
					import React from 'react';
 | 
				
			||||||
 | 
					import { connect } from 'react-redux';
 | 
				
			||||||
 | 
					import PropTypes from 'prop-types';
 | 
				
			||||||
 | 
					import { changeListEditorTitle, submitListEditor } from '../../../actions/lists';
 | 
				
			||||||
 | 
					import IconButton from '../../../components/icon_button';
 | 
				
			||||||
 | 
					import { defineMessages, injectIntl } from 'react-intl';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const messages = defineMessages({
 | 
				
			||||||
 | 
					  title: { id: 'lists.edit.submit', defaultMessage: 'Change title' },
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
 | 
					  value: state.getIn(['listEditor', 'title']),
 | 
				
			||||||
 | 
					  disabled: !state.getIn(['listEditor', 'isChanged']),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapDispatchToProps = dispatch => ({
 | 
				
			||||||
 | 
					  onChange: value => dispatch(changeListEditorTitle(value)),
 | 
				
			||||||
 | 
					  onSubmit: () => dispatch(submitListEditor(false)),
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default @connect(mapStateToProps, mapDispatchToProps)
 | 
				
			||||||
 | 
					@injectIntl
 | 
				
			||||||
 | 
					class ListForm extends React.PureComponent {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static propTypes = {
 | 
				
			||||||
 | 
					    value: PropTypes.string.isRequired,
 | 
				
			||||||
 | 
					    disabled: PropTypes.bool,
 | 
				
			||||||
 | 
					    intl: PropTypes.object.isRequired,
 | 
				
			||||||
 | 
					    onChange: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					    onSubmit: PropTypes.func.isRequired,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleChange = e => {
 | 
				
			||||||
 | 
					    this.props.onChange(e.target.value);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleSubmit = e => {
 | 
				
			||||||
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					    this.props.onSubmit();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  handleClick = () => {
 | 
				
			||||||
 | 
					    this.props.onSubmit();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  render () {
 | 
				
			||||||
 | 
					    const { value, disabled, intl } = this.props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const title = intl.formatMessage(messages.title);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      <form className='column-inline-form' onSubmit={this.handleSubmit}>
 | 
				
			||||||
 | 
					        <input
 | 
				
			||||||
 | 
					          className='setting-text'
 | 
				
			||||||
 | 
					          value={value}
 | 
				
			||||||
 | 
					          onChange={this.handleChange}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <IconButton
 | 
				
			||||||
 | 
					          disabled={disabled}
 | 
				
			||||||
 | 
					          icon='check'
 | 
				
			||||||
 | 
					          title={title}
 | 
				
			||||||
 | 
					          onClick={this.handleClick}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					      </form>
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -7,11 +7,11 @@ import { injectIntl } from 'react-intl';
 | 
				
			||||||
import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
 | 
					import { setupListEditor, clearListSuggestions, resetListEditor } from '../../actions/lists';
 | 
				
			||||||
import Account from './components/account';
 | 
					import Account from './components/account';
 | 
				
			||||||
import Search from './components/search';
 | 
					import Search from './components/search';
 | 
				
			||||||
 | 
					import EditListForm from './components/edit_list_form';
 | 
				
			||||||
import Motion from '../ui/util/optional_motion';
 | 
					import Motion from '../ui/util/optional_motion';
 | 
				
			||||||
import spring from 'react-motion/lib/spring';
 | 
					import spring from 'react-motion/lib/spring';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const mapStateToProps = state => ({
 | 
					const mapStateToProps = state => ({
 | 
				
			||||||
  title: state.getIn(['listEditor', 'title']),
 | 
					 | 
				
			||||||
  accountIds: state.getIn(['listEditor', 'accounts', 'items']),
 | 
					  accountIds: state.getIn(['listEditor', 'accounts', 'items']),
 | 
				
			||||||
  searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
 | 
					  searchAccountIds: state.getIn(['listEditor', 'suggestions', 'items']),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,6 @@ class ListEditor extends ImmutablePureComponent {
 | 
				
			||||||
    onInitialize: PropTypes.func.isRequired,
 | 
					    onInitialize: PropTypes.func.isRequired,
 | 
				
			||||||
    onClear: PropTypes.func.isRequired,
 | 
					    onClear: PropTypes.func.isRequired,
 | 
				
			||||||
    onReset: PropTypes.func.isRequired,
 | 
					    onReset: PropTypes.func.isRequired,
 | 
				
			||||||
    title: PropTypes.string.isRequired,
 | 
					 | 
				
			||||||
    accountIds: ImmutablePropTypes.list.isRequired,
 | 
					    accountIds: ImmutablePropTypes.list.isRequired,
 | 
				
			||||||
    searchAccountIds: ImmutablePropTypes.list.isRequired,
 | 
					    searchAccountIds: ImmutablePropTypes.list.isRequired,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
| 
						 | 
					@ -49,12 +48,12 @@ class ListEditor extends ImmutablePureComponent {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  render () {
 | 
					  render () {
 | 
				
			||||||
    const { title, accountIds, searchAccountIds, onClear } = this.props;
 | 
					    const { accountIds, searchAccountIds, onClear } = this.props;
 | 
				
			||||||
    const showSearch = searchAccountIds.size > 0;
 | 
					    const showSearch = searchAccountIds.size > 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className='modal-root__modal list-editor'>
 | 
					      <div className='modal-root__modal list-editor'>
 | 
				
			||||||
        <h4>{title}</h4>
 | 
					        <EditListForm />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <Search />
 | 
					        <Search />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,6 +22,7 @@ import {
 | 
				
			||||||
const initialState = ImmutableMap({
 | 
					const initialState = ImmutableMap({
 | 
				
			||||||
  listId: null,
 | 
					  listId: null,
 | 
				
			||||||
  isSubmitting: false,
 | 
					  isSubmitting: false,
 | 
				
			||||||
 | 
					  isChanged: false,
 | 
				
			||||||
  title: '',
 | 
					  title: '',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  accounts: ImmutableMap({
 | 
					  accounts: ImmutableMap({
 | 
				
			||||||
| 
						 | 
					@ -47,10 +48,16 @@ export default function listEditorReducer(state = initialState, action) {
 | 
				
			||||||
      map.set('isSubmitting', false);
 | 
					      map.set('isSubmitting', false);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  case LIST_EDITOR_TITLE_CHANGE:
 | 
					  case LIST_EDITOR_TITLE_CHANGE:
 | 
				
			||||||
    return state.set('title', action.value);
 | 
					    return state.withMutations(map => {
 | 
				
			||||||
 | 
					      map.set('title', action.value);
 | 
				
			||||||
 | 
					      map.set('isChanged', true);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  case LIST_CREATE_REQUEST:
 | 
					  case LIST_CREATE_REQUEST:
 | 
				
			||||||
  case LIST_UPDATE_REQUEST:
 | 
					  case LIST_UPDATE_REQUEST:
 | 
				
			||||||
    return state.set('isSubmitting', true);
 | 
					    return state.withMutations(map => {
 | 
				
			||||||
 | 
					      map.set('isSubmitting', true);
 | 
				
			||||||
 | 
					      map.set('isChanged', false);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
  case LIST_CREATE_FAIL:
 | 
					  case LIST_CREATE_FAIL:
 | 
				
			||||||
  case LIST_UPDATE_FAIL:
 | 
					  case LIST_UPDATE_FAIL:
 | 
				
			||||||
    return state.set('isSubmitting', false);
 | 
					    return state.set('isSubmitting', false);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,10 @@
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.rich-formatting a,
 | 
				
			||||||
 | 
					.rich-formatting p a,
 | 
				
			||||||
 | 
					.rich-formatting li a,
 | 
				
			||||||
 | 
					.landing-page__short-description p a,
 | 
				
			||||||
.status__content a,
 | 
					.status__content a,
 | 
				
			||||||
.reply-indicator__content a {
 | 
					.reply-indicator__content a {
 | 
				
			||||||
  color: lighten($ui-highlight-color, 12%);
 | 
					  color: lighten($ui-highlight-color, 12%);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -352,6 +352,8 @@
 | 
				
			||||||
.moved-account-widget,
 | 
					.moved-account-widget,
 | 
				
			||||||
.memoriam-widget,
 | 
					.memoriam-widget,
 | 
				
			||||||
.activity-stream,
 | 
					.activity-stream,
 | 
				
			||||||
.nothing-here {
 | 
					.nothing-here,
 | 
				
			||||||
 | 
					.directory__tag > a,
 | 
				
			||||||
 | 
					.directory__tag > div {
 | 
				
			||||||
  box-shadow: none;
 | 
					  box-shadow: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,3 +41,34 @@
 | 
				
			||||||
    font-size: 16px;
 | 
					    font-size: 16px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@mixin search-popout() {
 | 
				
			||||||
 | 
					  background: $simple-background-color;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  padding: 10px 14px;
 | 
				
			||||||
 | 
					  padding-bottom: 14px;
 | 
				
			||||||
 | 
					  margin-top: 10px;
 | 
				
			||||||
 | 
					  color: $light-text-color;
 | 
				
			||||||
 | 
					  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  h4 {
 | 
				
			||||||
 | 
					    text-transform: uppercase;
 | 
				
			||||||
 | 
					    color: $light-text-color;
 | 
				
			||||||
 | 
					    font-size: 13px;
 | 
				
			||||||
 | 
					    font-weight: 500;
 | 
				
			||||||
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  li {
 | 
				
			||||||
 | 
					    padding: 4px 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ul {
 | 
				
			||||||
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  em {
 | 
				
			||||||
 | 
					    font-weight: 500;
 | 
				
			||||||
 | 
					    color: $inverted-text-color;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,15 +49,9 @@ $small-breakpoint: 960px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  strong,
 | 
				
			||||||
  em {
 | 
					  em {
 | 
				
			||||||
    display: inline;
 | 
					 | 
				
			||||||
    margin: 0;
 | 
					 | 
				
			||||||
    padding: 0;
 | 
					 | 
				
			||||||
    font-weight: 700;
 | 
					    font-weight: 700;
 | 
				
			||||||
    background: transparent;
 | 
					 | 
				
			||||||
    font-family: inherit;
 | 
					 | 
				
			||||||
    font-size: inherit;
 | 
					 | 
				
			||||||
    line-height: inherit;
 | 
					 | 
				
			||||||
    color: lighten($darker-text-color, 10%);
 | 
					    color: lighten($darker-text-color, 10%);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -796,7 +790,7 @@ $small-breakpoint: 960px;
 | 
				
			||||||
      width: 100%;
 | 
					      width: 100%;
 | 
				
			||||||
      display: flex;
 | 
					      display: flex;
 | 
				
			||||||
      flex-direction: row-reverse;
 | 
					      flex-direction: row-reverse;
 | 
				
			||||||
      flex-wrap: wrap;
 | 
					      flex-wrap: nowrap;
 | 
				
			||||||
      justify-content: space-between;
 | 
					      justify-content: space-between;
 | 
				
			||||||
      align-items: center;
 | 
					      align-items: center;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					@ -846,14 +840,7 @@ $small-breakpoint: 960px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    strong {
 | 
					    strong {
 | 
				
			||||||
      display: inline;
 | 
					      font-weight: 500;
 | 
				
			||||||
      margin: 0;
 | 
					 | 
				
			||||||
      padding: 0;
 | 
					 | 
				
			||||||
      font-weight: 700;
 | 
					 | 
				
			||||||
      background: transparent;
 | 
					 | 
				
			||||||
      font-family: inherit;
 | 
					 | 
				
			||||||
      font-size: inherit;
 | 
					 | 
				
			||||||
      line-height: inherit;
 | 
					 | 
				
			||||||
      color: lighten($darker-text-color, 10%);
 | 
					      color: lighten($darker-text-color, 10%);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -476,7 +476,7 @@
 | 
				
			||||||
        opacity: 0;
 | 
					        opacity: 0;
 | 
				
			||||||
        transition: opacity .1s ease;
 | 
					        transition: opacity .1s ease;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        input {
 | 
					        textarea {
 | 
				
			||||||
          background: transparent;
 | 
					          background: transparent;
 | 
				
			||||||
          color: $secondary-text-color;
 | 
					          color: $secondary-text-color;
 | 
				
			||||||
          border: 0;
 | 
					          border: 0;
 | 
				
			||||||
| 
						 | 
					@ -3056,14 +3056,41 @@ a.status-card.compact:hover {
 | 
				
			||||||
  display: block;
 | 
					  display: block;
 | 
				
			||||||
  font-weight: 500;
 | 
					  font-weight: 500;
 | 
				
			||||||
  margin-bottom: 10px;
 | 
					  margin-bottom: 10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .column-settings__hashtag-select {
 | 
					.column-settings__hashtags {
 | 
				
			||||||
 | 
					  .column-settings__row {
 | 
				
			||||||
 | 
					    margin-bottom: 15px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .column-select {
 | 
				
			||||||
    &__control {
 | 
					    &__control {
 | 
				
			||||||
      @include search-input();
 | 
					      @include search-input();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__placeholder {
 | 
				
			||||||
 | 
					      color: $dark-text-color;
 | 
				
			||||||
 | 
					      padding-left: 2px;
 | 
				
			||||||
 | 
					      font-size: 12px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__value-container {
 | 
				
			||||||
 | 
					      padding-left: 6px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &__multi-value {
 | 
					    &__multi-value {
 | 
				
			||||||
      background: lighten($ui-base-color, 8%);
 | 
					      background: lighten($ui-base-color, 8%);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &__remove {
 | 
				
			||||||
 | 
					        cursor: pointer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        &:hover,
 | 
				
			||||||
 | 
					        &:active,
 | 
				
			||||||
 | 
					        &:focus {
 | 
				
			||||||
 | 
					          background: lighten($ui-base-color, 12%);
 | 
				
			||||||
 | 
					          color: lighten($darker-text-color, 4%);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &__multi-value__label,
 | 
					    &__multi-value__label,
 | 
				
			||||||
| 
						 | 
					@ -3071,9 +3098,42 @@ a.status-card.compact:hover {
 | 
				
			||||||
      color: $darker-text-color;
 | 
					      color: $darker-text-color;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    &__indicator-separator,
 | 
					    &__clear-indicator,
 | 
				
			||||||
    &__dropdown-indicator {
 | 
					    &__dropdown-indicator {
 | 
				
			||||||
      display: none;
 | 
					      cursor: pointer;
 | 
				
			||||||
 | 
					      transition: none;
 | 
				
			||||||
 | 
					      color: $dark-text-color;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &:hover,
 | 
				
			||||||
 | 
					      &:active,
 | 
				
			||||||
 | 
					      &:focus {
 | 
				
			||||||
 | 
					        color: lighten($dark-text-color, 4%);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__indicator-separator {
 | 
				
			||||||
 | 
					      background-color: lighten($ui-base-color, 8%);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__menu {
 | 
				
			||||||
 | 
					      @include search-popout();
 | 
				
			||||||
 | 
					      padding: 0;
 | 
				
			||||||
 | 
					      background: $ui-secondary-color;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__menu-list {
 | 
				
			||||||
 | 
					      padding: 6px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    &__option {
 | 
				
			||||||
 | 
					      color: $inverted-text-color;
 | 
				
			||||||
 | 
					      border-radius: 4px;
 | 
				
			||||||
 | 
					      font-size: 14px;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      &--is-focused,
 | 
				
			||||||
 | 
					      &--is-selected {
 | 
				
			||||||
 | 
					        background: darken($ui-secondary-color, 10%);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -4867,34 +4927,7 @@ a.status-card.compact:hover {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.search-popout {
 | 
					.search-popout {
 | 
				
			||||||
  background: $simple-background-color;
 | 
					  @include search-popout();
 | 
				
			||||||
  border-radius: 4px;
 | 
					 | 
				
			||||||
  padding: 10px 14px;
 | 
					 | 
				
			||||||
  padding-bottom: 14px;
 | 
					 | 
				
			||||||
  margin-top: 10px;
 | 
					 | 
				
			||||||
  color: $light-text-color;
 | 
					 | 
				
			||||||
  box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  h4 {
 | 
					 | 
				
			||||||
    text-transform: uppercase;
 | 
					 | 
				
			||||||
    color: $light-text-color;
 | 
					 | 
				
			||||||
    font-size: 13px;
 | 
					 | 
				
			||||||
    font-weight: 500;
 | 
					 | 
				
			||||||
    margin-bottom: 10px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  li {
 | 
					 | 
				
			||||||
    padding: 4px 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  ul {
 | 
					 | 
				
			||||||
    margin-bottom: 10px;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  em {
 | 
					 | 
				
			||||||
    font-weight: 500;
 | 
					 | 
				
			||||||
    color: $inverted-text-color;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
noscript {
 | 
					noscript {
 | 
				
			||||||
| 
						 | 
					@ -5130,7 +5163,7 @@ noscript {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  .icon-button {
 | 
					  .icon-button {
 | 
				
			||||||
    flex: 0 0 auto;
 | 
					    flex: 0 0 auto;
 | 
				
			||||||
    margin-left: 5px;
 | 
					    margin: 0 5px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,9 @@ class ActivityPub::Activity
 | 
				
			||||||
  include JsonLdHelper
 | 
					  include JsonLdHelper
 | 
				
			||||||
  include Redisable
 | 
					  include Redisable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  SUPPORTED_TYPES = %w(Note).freeze
 | 
				
			||||||
 | 
					  CONVERTED_TYPES = %w(Image Video Article Page).freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def initialize(json, account, **options)
 | 
					  def initialize(json, account, **options)
 | 
				
			||||||
    @json    = json
 | 
					    @json    = json
 | 
				
			||||||
    @account = account
 | 
					    @account = account
 | 
				
			||||||
| 
						 | 
					@ -71,6 +74,18 @@ class ActivityPub::Activity
 | 
				
			||||||
    @object_uri ||= value_or_id(@object)
 | 
					    @object_uri ||= value_or_id(@object)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def unsupported_object_type?
 | 
				
			||||||
 | 
					    @object.is_a?(String) || !(supported_object_type? || converted_object_type?)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def supported_object_type?
 | 
				
			||||||
 | 
					    equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def converted_object_type?
 | 
				
			||||||
 | 
					    equals_or_includes_any?(@object['type'], CONVERTED_TYPES)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def distribute(status)
 | 
					  def distribute(status)
 | 
				
			||||||
    crawl_links(status)
 | 
					    crawl_links(status)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -120,6 +135,23 @@ class ActivityPub::Activity
 | 
				
			||||||
    redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
 | 
					    redis.setex("delete_upon_arrival:#{@account.id}:#{uri}", 6.hours.seconds, uri)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def status_from_object
 | 
				
			||||||
 | 
					    # If the status is already known, return it
 | 
				
			||||||
 | 
					    status = status_from_uri(object_uri)
 | 
				
			||||||
 | 
					    return status unless status.nil?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # If the boosted toot is embedded and it is a self-boost, handle it like a Create
 | 
				
			||||||
 | 
					    unless unsupported_object_type?
 | 
				
			||||||
 | 
					      actor_id = value_or_id(first_of_value(@object['attributedTo'])) || @account.uri
 | 
				
			||||||
 | 
					      if actor_id == @account.uri
 | 
				
			||||||
 | 
					        return ActivityPub::Activity.factory({ 'type' => 'Create', 'actor' => actor_id, 'object' => @object }, @account).perform
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # If the status is not from the actor, try to fetch it
 | 
				
			||||||
 | 
					    return fetch_remote_original_status if value_or_id(first_of_value(@json['attributedTo'])) == @account.uri
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def fetch_remote_original_status
 | 
					  def fetch_remote_original_status
 | 
				
			||||||
    if object_uri.start_with?('http')
 | 
					    if object_uri.start_with?('http')
 | 
				
			||||||
      return if ActivityPub::TagManager.instance.local_uri?(object_uri)
 | 
					      return if ActivityPub::TagManager.instance.local_uri?(object_uri)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,9 +2,7 @@
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ActivityPub::Activity::Announce < ActivityPub::Activity
 | 
					class ActivityPub::Activity::Announce < ActivityPub::Activity
 | 
				
			||||||
  def perform
 | 
					  def perform
 | 
				
			||||||
    original_status   = status_from_uri(object_uri)
 | 
					    original_status = status_from_object
 | 
				
			||||||
    original_status ||= fetch_remote_original_status
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status)
 | 
					    return if original_status.nil? || delete_arrived_first?(@json['id']) || !announceable?(original_status)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    status = Status.find_by(account: @account, reblog: original_status)
 | 
					    status = Status.find_by(account: @account, reblog: original_status)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,8 @@
 | 
				
			||||||
# frozen_string_literal: true
 | 
					# frozen_string_literal: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ActivityPub::Activity::Create < ActivityPub::Activity
 | 
					class ActivityPub::Activity::Create < ActivityPub::Activity
 | 
				
			||||||
  SUPPORTED_TYPES = %w(Note).freeze
 | 
					 | 
				
			||||||
  CONVERTED_TYPES = %w(Image Video Article Page).freeze
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def perform
 | 
					  def perform
 | 
				
			||||||
    return if unsupported_object_type? || invalid_origin?(@object['id'])
 | 
					    return if unsupported_object_type? || invalid_origin?(@object['id']) || Tombstone.exists?(uri: @object['id']) || !related_to_local_activity?
 | 
				
			||||||
    return if Tombstone.exists?(uri: @object['id'])
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    RedisLock.acquire(lock_options) do |lock|
 | 
					    RedisLock.acquire(lock_options) do |lock|
 | 
				
			||||||
      if lock.acquired?
 | 
					      if lock.acquired?
 | 
				
			||||||
| 
						 | 
					@ -318,22 +314,10 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
 | 
				
			||||||
    @object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty?
 | 
					    @object['nameMap'].is_a?(Hash) && !@object['nameMap'].empty?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def unsupported_object_type?
 | 
					 | 
				
			||||||
    @object.is_a?(String) || !(supported_object_type? || converted_object_type?)
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def unsupported_media_type?(mime_type)
 | 
					  def unsupported_media_type?(mime_type)
 | 
				
			||||||
    mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
 | 
					    mime_type.present? && !(MediaAttachment::IMAGE_MIME_TYPES + MediaAttachment::VIDEO_MIME_TYPES).include?(mime_type)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def supported_object_type?
 | 
					 | 
				
			||||||
    equals_or_includes_any?(@object['type'], SUPPORTED_TYPES)
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def converted_object_type?
 | 
					 | 
				
			||||||
    equals_or_includes_any?(@object['type'], CONVERTED_TYPES)
 | 
					 | 
				
			||||||
  end
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  def skip_download?
 | 
					  def skip_download?
 | 
				
			||||||
    return @skip_download if defined?(@skip_download)
 | 
					    return @skip_download if defined?(@skip_download)
 | 
				
			||||||
    @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
 | 
					    @skip_download ||= DomainBlock.find_by(domain: @account.domain)&.reject_media?
 | 
				
			||||||
| 
						 | 
					@ -352,6 +336,37 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
 | 
				
			||||||
    !replied_to_status.nil? && replied_to_status.account.local?
 | 
					    !replied_to_status.nil? && replied_to_status.account.local?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def related_to_local_activity?
 | 
				
			||||||
 | 
					    fetch? || followed_by_local_accounts? || requested_through_relay? ||
 | 
				
			||||||
 | 
					      responds_to_followed_account? || addresses_local_accounts?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def fetch?
 | 
				
			||||||
 | 
					    !@options[:delivery]
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def followed_by_local_accounts?
 | 
				
			||||||
 | 
					    @account.passive_relationships.exists?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def requested_through_relay?
 | 
				
			||||||
 | 
					    @options[:relayed_through_account] && Relay.find_by(inbox_url: @options[:relayed_through_account].inbox_url)&.enabled?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def responds_to_followed_account?
 | 
				
			||||||
 | 
					    !replied_to_status.nil? && (replied_to_status.account.local? || replied_to_status.account.passive_relationships.exists?)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def addresses_local_accounts?
 | 
				
			||||||
 | 
					    return true if @options[:delivered_to_account_id]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    local_usernames = (as_array(@object['to']) + as_array(@object['cc'])).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return false if local_usernames.empty?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Account.local.where(username: local_usernames).exists?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def forward_for_reply
 | 
					  def forward_for_reply
 | 
				
			||||||
    return unless @json['signature'].present? && reply_to_local?
 | 
					    return unless @json['signature'].present? && reply_to_local?
 | 
				
			||||||
    ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
 | 
					    ActivityPub::RawDistributionWorker.perform_async(Oj.dump(@json), replied_to_status.account_id, [@account.preferred_inbox_url])
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -29,6 +29,7 @@ class Relay < ApplicationRecord
 | 
				
			||||||
    payload     = Oj.dump(follow_activity(activity_id))
 | 
					    payload     = Oj.dump(follow_activity(activity_id))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    update!(state: :pending, follow_activity_id: activity_id)
 | 
					    update!(state: :pending, follow_activity_id: activity_id)
 | 
				
			||||||
 | 
					    DeliveryFailureTracker.new(inbox_url).track_success!
 | 
				
			||||||
    ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
 | 
					    ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -37,6 +38,7 @@ class Relay < ApplicationRecord
 | 
				
			||||||
    payload     = Oj.dump(unfollow_activity(activity_id))
 | 
					    payload     = Oj.dump(unfollow_activity(activity_id))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    update!(state: :idle, follow_activity_id: nil)
 | 
					    update!(state: :idle, follow_activity_id: nil)
 | 
				
			||||||
 | 
					    DeliveryFailureTracker.new(inbox_url).track_success!
 | 
				
			||||||
    ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
 | 
					    ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,8 +3,8 @@
 | 
				
			||||||
class ActivityPub::ActivitySerializer < ActiveModel::Serializer
 | 
					class ActivityPub::ActivitySerializer < ActiveModel::Serializer
 | 
				
			||||||
  attributes :id, :type, :actor, :published, :to, :cc
 | 
					  attributes :id, :type, :actor, :published, :to, :cc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, unless: :announce?
 | 
					  has_one :proper, key: :object, serializer: ActivityPub::NoteSerializer, unless: :owned_announce?
 | 
				
			||||||
  attribute :proper_uri, key: :object, if: :announce?
 | 
					  attribute :proper_uri, key: :object, if: :owned_announce?
 | 
				
			||||||
  attribute :atom_uri, if: :announce?
 | 
					  attribute :atom_uri, if: :announce?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def id
 | 
					  def id
 | 
				
			||||||
| 
						 | 
					@ -42,4 +42,8 @@ class ActivityPub::ActivitySerializer < ActiveModel::Serializer
 | 
				
			||||||
  def announce?
 | 
					  def announce?
 | 
				
			||||||
    object.reblog?
 | 
					    object.reblog?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  def owned_announce?
 | 
				
			||||||
 | 
					    announce? && object.account == object.proper.account && object.proper.private_visibility?
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -44,6 +44,7 @@ class ActivityPub::ProcessCollectionService < BaseService
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def verify_account!
 | 
					  def verify_account!
 | 
				
			||||||
 | 
					    @options[:relayed_through_account] = @account
 | 
				
			||||||
    @account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
 | 
					    @account = ActivityPub::LinkedDataSignature.new(@json).verify_account!
 | 
				
			||||||
  rescue JSON::LD::JsonLdError => e
 | 
					  rescue JSON::LD::JsonLdError => e
 | 
				
			||||||
    Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}"
 | 
					    Rails.logger.debug "Could not verify LD-Signature for #{value_or_id(@json['actor'])}: #{e.message}"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,6 @@ class ActivityPub::ProcessingWorker
 | 
				
			||||||
  sidekiq_options backtrace: true
 | 
					  sidekiq_options backtrace: true
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def perform(account_id, body, delivered_to_account_id = nil)
 | 
					  def perform(account_id, body, delivered_to_account_id = nil)
 | 
				
			||||||
    ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true, delivered_to_account_id: delivered_to_account_id)
 | 
					    ActivityPub::ProcessCollectionService.new.call(body, Account.find(account_id), override_timestamps: true, delivered_to_account_id: delivered_to_account_id, delivery: true)
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,14 +46,14 @@ class Rack::Attack
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  throttle('throttle_authenticated_api', limit: 300, period: 5.minutes) do |req|
 | 
					  throttle('throttle_authenticated_api', limit: 300, period: 5.minutes) do |req|
 | 
				
			||||||
    req.api_request? && req.authenticated_user_id
 | 
					    req.authenticated_user_id if req.api_request?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  throttle('throttle_unauthenticated_api', limit: 7_500, period: 5.minutes) do |req|
 | 
					  throttle('throttle_unauthenticated_api', limit: 7_500, period: 5.minutes) do |req|
 | 
				
			||||||
    req.ip if req.api_request?
 | 
					    req.ip if req.api_request?
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  throttle('throttle_media', limit: 30, period: 30.minutes) do |req|
 | 
					  throttle('throttle_api_media', limit: 30, period: 30.minutes) do |req|
 | 
				
			||||||
    req.authenticated_user_id if req.post? && req.path.start_with?('/api/v1/media')
 | 
					    req.authenticated_user_id if req.post? && req.path.start_with?('/api/v1/media')
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -61,6 +61,13 @@ class Rack::Attack
 | 
				
			||||||
    req.ip if req.post? && req.path == '/api/v1/accounts'
 | 
					    req.ip if req.post? && req.path == '/api/v1/accounts'
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  API_DELETE_REBLOG_REGEX = /\A\/api\/v1\/statuses\/[\d]+\/unreblog/.freeze
 | 
				
			||||||
 | 
					  API_DELETE_STATUS_REGEX = /\A\/api\/v1\/statuses\/[\d]+/.freeze
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  throttle('throttle_api_delete', limit: 30, period: 30.minutes) do |req|
 | 
				
			||||||
 | 
					    req.authenticated_user_id if (req.post? && req.path =~ API_DELETE_REBLOG_REGEX) || (req.delete? && req.path =~ API_DELETE_STATUS_REGEX)
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  throttle('protected_paths', limit: 25, period: 5.minutes) do |req|
 | 
					  throttle('protected_paths', limit: 25, period: 5.minutes) do |req|
 | 
				
			||||||
    req.ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX
 | 
					    req.ip if req.post? && req.path =~ PROTECTED_PATHS_REGEX
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,7 +46,7 @@ cs:
 | 
				
			||||||
    choices_html: 'Volby uživatele %{name}:'
 | 
					    choices_html: 'Volby uživatele %{name}:'
 | 
				
			||||||
    follow: Sledovat
 | 
					    follow: Sledovat
 | 
				
			||||||
    followers:
 | 
					    followers:
 | 
				
			||||||
      few: Sledovatelé
 | 
					      few: Sledující
 | 
				
			||||||
      one: Sledující
 | 
					      one: Sledující
 | 
				
			||||||
      other: Sledujících
 | 
					      other: Sledujících
 | 
				
			||||||
    following: Sledovaných
 | 
					    following: Sledovaných
 | 
				
			||||||
| 
						 | 
					@ -618,7 +618,7 @@ cs:
 | 
				
			||||||
    lock_link: Zamkněte svůj účet
 | 
					    lock_link: Zamkněte svůj účet
 | 
				
			||||||
    purge: Odstranit ze sledujících
 | 
					    purge: Odstranit ze sledujících
 | 
				
			||||||
    success:
 | 
					    success:
 | 
				
			||||||
      few: V průběhu blokování sledovatelů ze %{count} domén...
 | 
					      few: V průběhu blokování sledujících ze %{count} domén...
 | 
				
			||||||
      one: V průběhu blokování sledujících z jedné domény...
 | 
					      one: V průběhu blokování sledujících z jedné domény...
 | 
				
			||||||
      other: V průběhu blokování sledujících z %{count} domén...
 | 
					      other: V průběhu blokování sledujících z %{count} domén...
 | 
				
			||||||
    true_privacy_html: Berte prosím na vědomí, že <strong>skutečného soukromí se dá dosáhnout pouze za pomoci end-to-end šifrování</strong>.
 | 
					    true_privacy_html: Berte prosím na vědomí, že <strong>skutečného soukromí se dá dosáhnout pouze za pomoci end-to-end šifrování</strong>.
 | 
				
			||||||
| 
						 | 
					@ -688,7 +688,7 @@ cs:
 | 
				
			||||||
      body: Zde najdete stručný souhrn zpráv, které jste zmeškal/a od vaší poslední návštěvy %{since}
 | 
					      body: Zde najdete stručný souhrn zpráv, které jste zmeškal/a od vaší poslední návštěvy %{since}
 | 
				
			||||||
      mention: "%{name} vás zmínil/a v:"
 | 
					      mention: "%{name} vás zmínil/a v:"
 | 
				
			||||||
      new_followers_summary:
 | 
					      new_followers_summary:
 | 
				
			||||||
        few: Navíc jste získal/a %{count} nové sledovatele, zatímco jste byl/a pryč! Skvělé!
 | 
					        few: Navíc jste získal/a %{count} nové sledující, zatímco jste byl/a pryč! Skvělé!
 | 
				
			||||||
        one: Navíc jste získal/a jednoho nového sledujícího, zatímco jste byl/a pryč! Hurá!
 | 
					        one: Navíc jste získal/a jednoho nového sledujícího, zatímco jste byl/a pryč! Hurá!
 | 
				
			||||||
        other: Navíc jste získal/a %{count} nových sledujících, zatímco jste byl/a pryč! Úžasné!
 | 
					        other: Navíc jste získal/a %{count} nových sledujících, zatímco jste byl/a pryč! Úžasné!
 | 
				
			||||||
      subject:
 | 
					      subject:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ WorkingDirectory=/home/mastodon/live
 | 
				
			||||||
Environment="NODE_ENV=production"
 | 
					Environment="NODE_ENV=production"
 | 
				
			||||||
Environment="PORT=4000"
 | 
					Environment="PORT=4000"
 | 
				
			||||||
Environment="STREAMING_CLUSTER_NUM=1"
 | 
					Environment="STREAMING_CLUSTER_NUM=1"
 | 
				
			||||||
ExecStart=/usr/bin/npm run start
 | 
					ExecStart=/usr/bin/node ./streaming
 | 
				
			||||||
TimeoutSec=15
 | 
					TimeoutSec=15
 | 
				
			||||||
Restart=always
 | 
					Restart=always
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,4 @@
 | 
				
			||||||
# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
 | 
					# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
 | 
				
			||||||
#
 | 
					
 | 
				
			||||||
# To ban all spiders from the entire site uncomment the next two lines:
 | 
					User-agent: *
 | 
				
			||||||
# User-agent: *
 | 
					Disallow: /media_proxy/
 | 
				
			||||||
# Disallow: /
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,7 @@
 | 
				
			||||||
require 'rails_helper'
 | 
					require 'rails_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RSpec.describe ActivityPub::Activity::Announce do
 | 
					RSpec.describe ActivityPub::Activity::Announce do
 | 
				
			||||||
  let(:sender)    { Fabricate(:account) }
 | 
					  let(:sender)    { Fabricate(:account, followers_url: 'http://example.com/followers') }
 | 
				
			||||||
  let(:recipient) { Fabricate(:account) }
 | 
					  let(:recipient) { Fabricate(:account) }
 | 
				
			||||||
  let(:status)    { Fabricate(:status, account: recipient) }
 | 
					  let(:status)    { Fabricate(:status, account: recipient) }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,19 +11,60 @@ RSpec.describe ActivityPub::Activity::Announce do
 | 
				
			||||||
      id: 'foo',
 | 
					      id: 'foo',
 | 
				
			||||||
      type: 'Announce',
 | 
					      type: 'Announce',
 | 
				
			||||||
      actor: ActivityPub::TagManager.instance.uri_for(sender),
 | 
					      actor: ActivityPub::TagManager.instance.uri_for(sender),
 | 
				
			||||||
      object: ActivityPub::TagManager.instance.uri_for(status),
 | 
					      object: object_json,
 | 
				
			||||||
    }.with_indifferent_access
 | 
					    }.with_indifferent_access
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe '#perform' do
 | 
					  subject { described_class.new(json, sender) }
 | 
				
			||||||
    subject { described_class.new(json, sender) }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  before do
 | 
				
			||||||
 | 
					    sender.update(uri: ActivityPub::TagManager.instance.uri_for(sender))
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe '#perform' do
 | 
				
			||||||
    before do
 | 
					    before do
 | 
				
			||||||
      subject.perform
 | 
					      subject.perform
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it 'creates a reblog by sender of status' do
 | 
					    context 'a known status' do
 | 
				
			||||||
      expect(sender.reblogged?(status)).to be true
 | 
					      let(:object_json) do
 | 
				
			||||||
 | 
					        ActivityPub::TagManager.instance.uri_for(status)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'creates a reblog by sender of status' do
 | 
				
			||||||
 | 
					        expect(sender.reblogged?(status)).to be true
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'self-boost of a previously unknown status with missing attributedTo' do
 | 
				
			||||||
 | 
					      let(:object_json) do
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
 | 
				
			||||||
 | 
					          type: 'Note',
 | 
				
			||||||
 | 
					          content: 'Lorem ipsum',
 | 
				
			||||||
 | 
					          to: 'http://example.com/followers',
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'creates a reblog by sender of status' do
 | 
				
			||||||
 | 
					        expect(sender.reblogged?(sender.statuses.first)).to be true
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'self-boost of a previously unknown status with correct attributedTo' do
 | 
				
			||||||
 | 
					      let(:object_json) do
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
 | 
				
			||||||
 | 
					          type: 'Note',
 | 
				
			||||||
 | 
					          content: 'Lorem ipsum',
 | 
				
			||||||
 | 
					          attributedTo: ActivityPub::TagManager.instance.uri_for(sender),
 | 
				
			||||||
 | 
					          to: 'http://example.com/followers',
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'creates a reblog by sender of status' do
 | 
				
			||||||
 | 
					        expect(sender.reblogged?(sender.statuses.first)).to be true
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue