Merge commit '6a3f6d5876' into kb_migration_development

This commit is contained in:
KMY 2023-04-16 18:13:16 +09:00
commit 64e0e64694
684 changed files with 16574 additions and 8379 deletions

View file

@ -2,7 +2,7 @@
module Admin
class DomainBlocksController < BaseController
before_action :set_domain_block, only: [:show, :destroy, :edit, :update]
before_action :set_domain_block, only: [:destroy, :edit, :update]
def batch
authorize :domain_block, :create?

View file

@ -2,8 +2,6 @@
module Admin
class EmailDomainBlocksController < BaseController
before_action :set_email_domain_block, only: [:show, :destroy]
def index
authorize :email_domain_block, :index?
@ -59,10 +57,6 @@ module Admin
private
def set_email_domain_block
@email_domain_block = EmailDomainBlock.find(params[:id])
end
def set_resolved_records
Resolv::DNS.open do |dns|
dns.timeouts = 5

View file

@ -13,7 +13,7 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
def update
@account = current_account
UpdateAccountService.new.call(@account, account_params, raise_error: true)
UserSettingsDecorator.new(current_user).update(user_settings_params) if user_settings_params
current_user.update(user_params) if user_params
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
render json: @account, serializer: REST::CredentialAccountSerializer
end
@ -35,15 +35,18 @@ class Api::V1::Accounts::CredentialsController < Api::BaseController
)
end
def user_settings_params
def user_params
return nil if params[:source].blank?
source_params = params.require(:source)
{
'setting_default_privacy' => source_params.fetch(:privacy, @account.user.setting_default_privacy),
'setting_default_sensitive' => source_params.fetch(:sensitive, @account.user.setting_default_sensitive),
'setting_default_language' => source_params.fetch(:language, @account.user.setting_default_language),
settings_attributes: {
default_privacy: source_params.fetch(:privacy, @account.user.setting_default_privacy),
default_searchability: source_params.fetch(:searchability, @account.user.setting_default_searchability),
default_sensitive: source_params.fetch(:sensitive, @account.user.setting_default_sensitive),
default_language: source_params.fetch(:language, @account.user.setting_default_language),
},
}
end
end

View file

@ -0,0 +1,23 @@
# frozen_string_literal: true
class Api::V1::Instances::TranslationLanguagesController < Api::BaseController
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
before_action :set_languages
def show
expires_in 1.day, public: true
render json: @languages
end
private
def set_languages
if TranslationService.configured?
@languages = Rails.cache.fetch('translation_service/languages', expires_in: 7.days, race_condition_ttl: 1.hour) { TranslationService.configured.languages }
@languages['und'] = @languages.delete(nil) if @languages.key?(nil)
else
@languages = {}
end
end
end

View file

@ -5,7 +5,7 @@ class Api::V1::StreamingController < Api::BaseController
if Rails.configuration.x.streaming_api_base_url == request.host
not_found
else
redirect_to streaming_api_url, status: 301
redirect_to streaming_api_url, status: 301, allow_other_host: true
end
end

View file

@ -16,6 +16,8 @@ class ApplicationController < ActionController::Base
helper_method :current_theme
helper_method :single_user_mode?
helper_method :use_seamless_external_login?
helper_method :omniauth_only?
helper_method :sso_account_settings
helper_method :whitelist_mode?
rescue_from ActionController::ParameterMissing, Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
@ -61,7 +63,11 @@ class ApplicationController < ActionController::Base
end
def after_sign_out_path_for(_resource_or_scope)
new_user_session_path
if ENV['OMNIAUTH_ONLY'] == 'true' && ENV['OIDC_ENABLED'] == 'true'
'/auth/auth/openid_connect/logout'
else
new_user_session_path
end
end
protected
@ -114,6 +120,14 @@ class ApplicationController < ActionController::Base
Devise.pam_authentication || Devise.ldap_authentication
end
def omniauth_only?
ENV['OMNIAUTH_ONLY'] == 'true'
end
def sso_account_settings
ENV.fetch('SSO_ACCOUNT_SETTINGS')
end
def current_account
return @current_account if defined?(@current_account)

View file

@ -13,7 +13,7 @@ class BackupsController < ApplicationController
when :s3
redirect_to @backup.dump.expiring_url(10)
when :fog
if Paperclip::Attachment.default_options.dig(:storage, :fog_credentials, :openstack_temp_url_key).present?
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
redirect_to @backup.dump.expiring_url(Time.now.utc + 10)
else
redirect_to full_asset_url(@backup.dump.url)

View file

@ -10,7 +10,8 @@ module AccountControllerConcern
included do
before_action :set_instance_presenter
before_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
after_action :set_link_headers, if: -> { request.format.nil? || request.format == :html }
end
private

View file

@ -3,6 +3,158 @@
module CacheConcern
extend ActiveSupport::Concern
module ActiveRecordCoder
EMPTY_HASH = {}.freeze
class << self
def dump(record)
instances = InstanceTracker.new
serialized_associations = serialize_associations(record, instances)
serialized_records = instances.map { |r| serialize_record(r) }
[serialized_associations, *serialized_records]
end
def load(payload)
instances = InstanceTracker.new
serialized_associations, *serialized_records = payload
serialized_records.each { |attrs| instances.push(deserialize_record(*attrs)) }
deserialize_associations(serialized_associations, instances)
end
private
# Records without associations, or which have already been visited before,
# are serialized by their id alone.
#
# Records with associations are serialized as a two-element array including
# their id and the record's association cache.
#
def serialize_associations(record, instances)
return unless record
if (id = instances.lookup(record))
payload = id
else
payload = instances.push(record)
cached_associations = record.class.reflect_on_all_associations.select do |reflection|
record.association_cached?(reflection.name)
end
unless cached_associations.empty?
serialized_associations = cached_associations.map do |reflection|
association = record.association(reflection.name)
serialized_target = if reflection.collection?
association.target.map { |target_record| serialize_associations(target_record, instances) }
else
serialize_associations(association.target, instances)
end
[reflection.name, serialized_target]
end
payload = [payload, serialized_associations]
end
end
payload
end
def deserialize_associations(payload, instances)
return unless payload
id, associations = payload
record = instances.fetch(id)
associations&.each do |name, serialized_target|
begin
association = record.association(name)
rescue ActiveRecord::AssociationNotFoundError
raise AssociationMissingError, "undefined association: #{name}"
end
target = if association.reflection.collection?
serialized_target.map! { |serialized_record| deserialize_associations(serialized_record, instances) }
else
deserialize_associations(serialized_target, instances)
end
association.target = target
end
record
end
def serialize_record(record)
arguments = [record.class.name, attributes_for_database(record)]
arguments << true if record.new_record?
arguments
end
if Rails.gem_version >= Gem::Version.new('7.0')
def attributes_for_database(record)
attributes = record.attributes_for_database
attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr }
attributes
end
else
def attributes_for_database(record)
attributes = record.instance_variable_get(:@attributes).send(:attributes).transform_values(&:value_for_database)
attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr }
attributes
end
end
def deserialize_record(class_name, attributes_from_database, new_record = false) # rubocop:disable Style/OptionalBooleanParameter
begin
klass = Object.const_get(class_name)
rescue NameError
raise ClassMissingError, "undefined class: #{class_name}"
end
# Ideally we'd like to call `klass.instantiate`, however it doesn't allow to pass
# wether the record was persisted or not.
attributes = klass.attributes_builder.build_from_database(attributes_from_database, EMPTY_HASH)
klass.allocate.init_with_attributes(attributes, new_record)
end
end
class Error < StandardError
end
class ClassMissingError < Error
end
class AssociationMissingError < Error
end
class InstanceTracker
def initialize
@instances = []
@ids = {}.compare_by_identity
end
def map(&block)
@instances.map(&block)
end
def fetch(...)
@instances.fetch(...)
end
def push(instance)
id = @ids[instance] = @instances.size
@instances << instance
id
end
def lookup(instance)
@ids[instance]
end
end
end
def render_with_cache(**options)
raise ArgumentError, 'only JSON render calls are supported' unless options.key?(:json) || block_given?
@ -34,8 +186,13 @@ module CacheConcern
raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
return [] if raw.empty?
cached_keys_with_value = Rails.cache.read_multi(*raw).transform_keys(&:id)
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
cached_keys_with_value = begin
Rails.cache.read_multi(*raw).transform_keys(&:id).transform_values { |r| ActiveRecordCoder.load(r) }
rescue ActiveRecordCoder::Error
{} # The serialization format may have changed, let's pretend it's a cache miss.
end
uncached_ids = raw.map(&:id) - cached_keys_with_value.keys
klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)
@ -43,7 +200,7 @@ module CacheConcern
uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id)
uncached.each_value do |item|
Rails.cache.write(item, item)
Rails.cache.write(item, ActiveRecordCoder.dump(item))
end
end

View file

@ -138,7 +138,7 @@ module SignatureVerification
end
def signed_headers
signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split(' ')
signature_params.fetch('headers', signature_algorithm == 'hs2019' ? '(created)' : 'date').downcase.split
end
def verify_signature_strength!

View file

@ -23,7 +23,7 @@ class MediaProxyController < ApplicationController
redownload! if @media_attachment.needs_redownload? && !reject_media?
end
redirect_to full_asset_url(@media_attachment.file.url(version))
redirect_to full_asset_url(@media_attachment.file.url(version)), allow_other_host: true
end
private

View file

@ -4,8 +4,6 @@ class Settings::PreferencesController < Settings::BaseController
def show; end
def update
user_settings.update(user_settings_params.to_h)
if current_user.update(user_params)
I18n.locale = current_user.locale
redirect_to after_update_redirect_path, notice: I18n.t('generic.changes_saved_msg')
@ -20,46 +18,7 @@ class Settings::PreferencesController < Settings::BaseController
settings_preferences_path
end
def user_settings
UserSettingsDecorator.new(current_user)
end
def user_params
params.require(:user).permit(
:locale,
chosen_languages: []
)
end
def user_settings_params
params.require(:user).permit(
:setting_default_privacy,
:setting_default_searchability,
:setting_default_sensitive,
:setting_public_post_to_unlisted,
:setting_default_language,
:setting_unfollow_modal,
:setting_boost_modal,
:setting_delete_modal,
:setting_auto_play_gif,
:setting_display_media,
:setting_display_media_expand,
:setting_expand_spoilers,
:setting_reduce_motion,
:setting_disable_swiping,
:setting_system_font_ui,
:setting_noindex,
:setting_theme,
:setting_aggregate_reblogs,
:setting_show_application,
:setting_advanced_layout,
:setting_use_blurhash,
:setting_use_pending_items,
:setting_trends,
:setting_crop_images,
:setting_always_send_emails,
notification_emails: %i(follow follow_request reblog favourite mention report pending_account trending_tag appeal),
interactions: %i(must_be_follower must_be_following must_be_following_dm)
)
params.require(:user).permit(:locale, chosen_languages: [], settings_attributes: UserSettings.keys)
end
end

View file

@ -22,18 +22,9 @@ module Settings
private
def confirmation_params
params.require(:form_two_factor_confirmation).permit(:otp_attempt)
end
def verify_otp_not_enabled
redirect_to settings_two_factor_authentication_methods_path if current_user.otp_enabled?
end
def acceptable_code?
current_user.validate_and_consume_otp!(confirmation_params[:otp_attempt]) ||
current_user.invalidate_otp_backup_code!(confirmation_params[:otp_attempt])
end
end
end
end

View file

@ -9,11 +9,12 @@ class StatusesController < ApplicationController
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_status
before_action :set_instance_presenter
before_action :set_link_headers
before_action :redirect_to_original, only: :show
before_action :set_cache_headers
before_action :set_body_classes, only: :embed
after_action :set_link_headers
skip_around_action :set_locale, if: -> { request.format == :json }
skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode?
@ -70,6 +71,6 @@ class StatusesController < ApplicationController
end
def redirect_to_original
redirect_to ActivityPub::TagManager.instance.url_for(@status.reblog) if @status.reblog?
redirect_to(ActivityPub::TagManager.instance.url_for(@status.reblog), allow_other_host: true) if @status.reblog?
end
end

View file

@ -112,7 +112,7 @@ module ApplicationHelper
def fa_icon(icon, attributes = {})
class_names = attributes[:class]&.split(' ') || []
class_names << 'fa'
class_names += icon.split(' ').map { |cl| "fa-#{cl}" }
class_names += icon.split.map { |cl| "fa-#{cl}" }
content_tag(:i, nil, attributes.merge(class: class_names.join(' ')))
end
@ -166,7 +166,7 @@ module ApplicationHelper
end
def body_classes
output = (@body_classes || '').split(' ')
output = (@body_classes || '').split
output << "theme-#{current_theme.parameterize}"
output << 'system-font' if current_account&.user&.setting_system_font_ui
output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion')

View file

@ -8,7 +8,7 @@ module HomeHelper
end
def account_link_to(account, button = '', path: nil)
content_tag(:div, class: 'account') do
content_tag(:div, class: 'account account--minimal') do
content_tag(:div, class: 'account__wrapper') do
section = if account.nil?
content_tag(:div, class: 'account__display-name') do

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true
# rubocop:disable Metrics/ModuleLength, Style/WordArray
# rubocop:disable Metrics/ModuleLength
module LanguagesHelper
ISO_639_1 = {
@ -275,4 +275,4 @@ module LanguagesHelper
end
end
# rubocop:enable Metrics/ModuleLength, Style/WordArray
# rubocop:enable Metrics/ModuleLength

View file

@ -0,0 +1,17 @@
import { useCallback, useState } from 'react';
export const useHovering = (animate?: boolean) => {
const [hovering, setHovering] = useState<boolean>(animate ?? false);
const handleMouseEnter = useCallback(() => {
if (animate) return;
setHovering(true);
}, [animate]);
const handleMouseLeave = useCallback(() => {
if (animate) return;
setHovering(false);
}, [animate]);
return { hovering, handleMouseEnter, handleMouseLeave };
};

View file

@ -4,7 +4,6 @@ import { defineMessages } from 'react-intl';
import api from 'mastodon/api';
import { search as emojiSearch } from 'mastodon/features/emoji/emoji_mart_search_light';
import { tagHistory } from 'mastodon/settings';
import resizeImage from 'mastodon/utils/resize_image';
import { showAlert, showAlertForError } from './alerts';
import { useEmoji } from './emojis';
import { importFetchedAccounts, importFetchedStatus } from './importer';
@ -279,46 +278,42 @@ export function uploadCompose(files) {
dispatch(uploadComposeRequest());
for (const [i, f] of Array.from(files).entries()) {
for (const [i, file] of Array.from(files).entries()) {
if (media.size + i >= 4) break;
resizeImage(f).then(file => {
const data = new FormData();
data.append('file', file);
// Account for disparity in size of original image and resized data
total += file.size - f.size;
const data = new FormData();
data.append('file', file);
return api(getState).post('/api/v2/media', data, {
onUploadProgress: function({ loaded }){
progress[i] = loaded;
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
},
}).then(({ status, data }) => {
// If server-side processing of the media attachment has not completed yet,
// poll the server until it is, before showing the media attachment as uploaded
api(getState).post('/api/v2/media', data, {
onUploadProgress: function({ loaded }){
progress[i] = loaded;
dispatch(uploadComposeProgress(progress.reduce((a, v) => a + v, 0), total));
},
}).then(({ status, data }) => {
// If server-side processing of the media attachment has not completed yet,
// poll the server until it is, before showing the media attachment as uploaded
if (status === 200) {
dispatch(uploadComposeSuccess(data, f));
} else if (status === 202) {
dispatch(uploadComposeProcessing());
if (status === 200) {
dispatch(uploadComposeSuccess(data, file));
} else if (status === 202) {
dispatch(uploadComposeProcessing());
let tryCount = 1;
let tryCount = 1;
const poll = () => {
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
if (response.status === 200) {
dispatch(uploadComposeSuccess(response.data, f));
} else if (response.status === 206) {
const retryAfter = (Math.log2(tryCount) || 1) * 1000;
tryCount += 1;
setTimeout(() => poll(), retryAfter);
}
}).catch(error => dispatch(uploadComposeFail(error)));
};
const poll = () => {
api(getState).get(`/api/v1/media/${data.id}`).then(response => {
if (response.status === 200) {
dispatch(uploadComposeSuccess(response.data, file));
} else if (response.status === 206) {
const retryAfter = (Math.log2(tryCount) || 1) * 1000;
tryCount += 1;
setTimeout(() => poll(), retryAfter);
}
}).catch(error => dispatch(uploadComposeFail(error)));
};
poll();
}
});
poll();
}
}).catch(error => dispatch(uploadComposeFail(error)));
}
};

View file

@ -55,7 +55,7 @@ export const synchronouslySubmitMarkers = () => (dispatch, getState) => {
client.open('POST', '/api/v1/markers', false);
client.setRequestHeader('Content-Type', 'application/json');
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
client.SUBMIT(JSON.stringify(params));
client.send(JSON.stringify(params));
} catch (e) {
// Do not make the BeforeUnload handler error out
}

View file

@ -23,6 +23,7 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
* @return {object}
*/
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
// @ts-expect-error
return (dispatch, getState) => {
// Do not open a player for a toot that does not exist
if (getState().hasIn(['statuses', statusId])) {

View file

@ -14,6 +14,9 @@ export const SEARCH_EXPAND_REQUEST = 'SEARCH_EXPAND_REQUEST';
export const SEARCH_EXPAND_SUCCESS = 'SEARCH_EXPAND_SUCCESS';
export const SEARCH_EXPAND_FAIL = 'SEARCH_EXPAND_FAIL';
export const SEARCH_RESULT_CLICK = 'SEARCH_RESULT_CLICK';
export const SEARCH_RESULT_FORGET = 'SEARCH_RESULT_FORGET';
export function changeSearch(value) {
return {
type: SEARCH_CHANGE,
@ -27,7 +30,7 @@ export function clearSearch() {
};
}
export function submitSearch() {
export function submitSearch(type) {
return (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
const signedIn = !!getState().getIn(['meta', 'me']);
@ -44,6 +47,7 @@ export function submitSearch() {
q: value,
resolve: signedIn,
limit: 10,
type,
},
}).then(response => {
if (response.data.accounts) {
@ -130,3 +134,42 @@ export const expandSearchFail = error => ({
export const showSearch = () => ({
type: SEARCH_SHOW,
});
export const openURL = routerHistory => (dispatch, getState) => {
const value = getState().getIn(['search', 'value']);
const signedIn = !!getState().getIn(['meta', 'me']);
if (!signedIn) {
return;
}
dispatch(fetchSearchRequest());
api(getState).get('/api/v2/search', { params: { q: value, resolve: true } }).then(response => {
if (response.data.accounts?.length > 0) {
dispatch(importFetchedAccounts(response.data.accounts));
routerHistory.push(`/@${response.data.accounts[0].acct}`);
} else if (response.data.statuses?.length > 0) {
dispatch(importFetchedStatuses(response.data.statuses));
routerHistory.push(`/@${response.data.statuses[0].account.acct}/${response.data.statuses[0].id}`);
}
dispatch(fetchSearchSuccess(response.data, value));
}).catch(err => {
dispatch(fetchSearchFail(err));
});
};
export const clickSearchResult = (q, type) => ({
type: SEARCH_RESULT_CLICK,
result: {
type,
q,
},
});
export const forgetSearchResult = q => ({
type: SEARCH_RESULT_FORGET,
q,
});

View file

@ -5,6 +5,10 @@ export const SERVER_FETCH_REQUEST = 'Server_FETCH_REQUEST';
export const SERVER_FETCH_SUCCESS = 'Server_FETCH_SUCCESS';
export const SERVER_FETCH_FAIL = 'Server_FETCH_FAIL';
export const SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST = 'SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST';
export const SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS = 'SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS';
export const SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL = 'SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL';
export const EXTENDED_DESCRIPTION_REQUEST = 'EXTENDED_DESCRIPTION_REQUEST';
export const EXTENDED_DESCRIPTION_SUCCESS = 'EXTENDED_DESCRIPTION_SUCCESS';
export const EXTENDED_DESCRIPTION_FAIL = 'EXTENDED_DESCRIPTION_FAIL';
@ -37,6 +41,29 @@ const fetchServerFail = error => ({
error,
});
export const fetchServerTranslationLanguages = () => (dispatch, getState) => {
dispatch(fetchServerTranslationLanguagesRequest());
api(getState)
.get('/api/v1/instance/translation_languages').then(({ data }) => {
dispatch(fetchServerTranslationLanguagesSuccess(data));
}).catch(err => dispatch(fetchServerTranslationLanguagesFail(err)));
};
const fetchServerTranslationLanguagesRequest = () => ({
type: SERVER_TRANSLATION_LANGUAGES_FETCH_REQUEST,
});
const fetchServerTranslationLanguagesSuccess = translationLanguages => ({
type: SERVER_TRANSLATION_LANGUAGES_FETCH_SUCCESS,
translationLanguages,
});
const fetchServerTranslationLanguagesFail = error => ({
type: SERVER_TRANSLATION_LANGUAGES_FETCH_FAIL,
error,
});
export const fetchExtendedDescription = () => (dispatch, getState) => {
dispatch(fetchExtendedDescriptionRequest());

View file

@ -46,6 +46,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
connectStream(channelName, params, (dispatch, getState) => {
const locale = getState().getIn(['meta', 'locale']);
// @ts-expect-error
let pollingId;
/**
@ -61,9 +62,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
onConnect() {
dispatch(connectTimeline(timelineId));
// @ts-expect-error
if (pollingId) {
clearTimeout(pollingId);
pollingId = null;
// @ts-ignore
clearTimeout(pollingId); pollingId = null;
}
if (options.fillGaps) {
@ -75,34 +77,41 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
dispatch(disconnectTimeline(timelineId));
if (options.fallback) {
// @ts-expect-error
pollingId = setTimeout(() => useFallback(options.fallback), randomUpTo(40000));
}
},
onReceive (data) {
switch(data.event) {
onReceive(data) {
switch (data.event) {
case 'update':
// @ts-expect-error
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));
break;
case 'status.update':
// @ts-expect-error
dispatch(updateStatus(JSON.parse(data.payload)));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
// @ts-expect-error
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
break;
case 'emoji_reaction':
dispatch(updateEmojiReactions(JSON.parse(data.payload), getState().getIn(['meta', 'me'])));
break;
case 'conversation':
// @ts-expect-error
dispatch(updateConversations(JSON.parse(data.payload)));
break;
case 'announcement':
// @ts-expect-error
dispatch(updateAnnouncements(JSON.parse(data.payload)));
break;
case 'announcement.reaction':
// @ts-expect-error
dispatch(updateAnnouncementsReaction(JSON.parse(data.payload)));
break;
case 'announcement.delete':
@ -118,7 +127,9 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
* @param {function(): void} done
*/
const refreshHomeTimelineAndNotification = (dispatch, done) => {
// @ts-expect-error
dispatch(expandHomeTimeline({}, () =>
// @ts-expect-error
dispatch(expandNotifications({}, () =>
dispatch(fetchAnnouncements(done))))));
};
@ -127,6 +138,7 @@ const refreshHomeTimelineAndNotification = (dispatch, done) => {
* @return {function(): void}
*/
export const connectUserStream = () =>
// @ts-expect-error
connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps });
/**

View file

@ -36,7 +36,7 @@ const setCSRFHeader = () => {
ready(setCSRFHeader);
/**
* @param {() => import('immutable').Map} getState
* @param {() => import('immutable').Map<string,any>} getState
* @returns {import('axios').RawAxiosRequestHeaders}
*/
const authorizationHeaderFromState = getState => {
@ -52,7 +52,7 @@ const authorizationHeaderFromState = getState => {
};
/**
* @param {() => import('immutable').Map} getState
* @param {() => import('immutable').Map<string,any>} getState
* @returns {import('axios').AxiosInstance}
*/
export default function api(getState) {

View file

@ -1,17 +1,11 @@
import 'intl';
import 'intl/locale-data/jsonp/en';
import 'es6-symbol/implement';
import includes from 'array-includes';
import assign from 'object-assign';
import values from 'object.values';
import isNaN from 'is-nan';
import { decode as decodeBase64 } from './utils/base64';
import promiseFinally from 'promise.prototype.finally';
if (!Array.prototype.includes) {
includes.shim();
}
if (!Object.assign) {
Object.assign = assign;
}
@ -20,10 +14,6 @@ if (!Object.values) {
values.shim();
}
if (!Number.isNaN) {
Number.isNaN = isNaN;
}
promiseFinally.shim();
if (!HTMLCanvasElement.prototype.toBlob) {

View file

@ -1,4 +1,4 @@
import React, { Fragment } from 'react';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
@ -10,6 +10,10 @@ import { me } from '../initial_state';
import RelativeTimestamp from './relative_timestamp';
import Skeleton from 'mastodon/components/skeleton';
import { Link } from 'react-router-dom';
import { counterRenderer } from 'mastodon/components/common_counter';
import ShortNumber from 'mastodon/components/short_number';
import Icon from 'mastodon/components/icon';
import classNames from 'classnames';
const messages = defineMessages({
follow: { id: 'account.follow', defaultMessage: 'Follow' },
@ -23,7 +27,26 @@ const messages = defineMessages({
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
});
export default @injectIntl
class VerifiedBadge extends React.PureComponent {
static propTypes = {
link: PropTypes.string.isRequired,
verifiedAt: PropTypes.string.isRequired,
};
render () {
const { link } = this.props;
return (
<span className='verified-badge'>
<Icon id='check' className='verified-badge__mark' />
<span dangerouslySetInnerHTML={{ __html: link }} />
</span>
);
}
}
class Account extends ImmutablePureComponent {
static propTypes = {
@ -35,6 +58,7 @@ class Account extends ImmutablePureComponent {
onMuteNotifications: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hidden: PropTypes.bool,
minimal: PropTypes.bool,
actionIcon: PropTypes.string,
actionTitle: PropTypes.string,
defaultAction: PropTypes.string,
@ -71,15 +95,19 @@ class Account extends ImmutablePureComponent {
};
render () {
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size, children } = this.props;
const { account, intl, hidden, onActionClick, actionIcon, actionTitle, defaultAction, size, minimal, children } = this.props;
if (!account) {
return (
<div className='account'>
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<div className='account__display-name'>
<div className='account__avatar-wrapper'><Skeleton width={36} height={36} /></div>
<DisplayName />
<div className='account__avatar-wrapper'><Skeleton width={size} height={size} /></div>
<div>
<DisplayName />
<Skeleton width='7ch' />
</div>
</div>
</div>
</div>
@ -88,10 +116,10 @@ class Account extends ImmutablePureComponent {
if (hidden) {
return (
<Fragment>
<>
{account.get('display_name')}
{account.get('username')}
</Fragment>
</>
);
}
@ -119,10 +147,10 @@ class Account extends ImmutablePureComponent {
hidingNotificationsButton = <IconButton active icon='bell-slash' title={intl.formatMessage(messages.mute_notifications, { name: account.get('username') })} onClick={this.handleMuteNotifications} />;
}
buttons = (
<Fragment>
<>
<IconButton active icon='volume-up' title={intl.formatMessage(messages.unmute, { name: account.get('username') })} onClick={this.handleMute} />
{hidingNotificationsButton}
</Fragment>
</>
);
} else if (defaultAction === 'mute') {
buttons = <IconButton icon='volume-off' title={intl.formatMessage(messages.mute, { name: account.get('username') })} onClick={this.handleMute} />;
@ -133,30 +161,49 @@ class Account extends ImmutablePureComponent {
}
}
let mute_expires_at;
let muteTimeRemaining;
if (account.get('mute_expires_at')) {
mute_expires_at = <div><RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></div>;
muteTimeRemaining = <>· <RelativeTimestamp timestamp={account.get('mute_expires_at')} futureDate /></>;
}
let verification;
const firstVerifiedField = account.get('fields').find(item => !!item.get('verified_at'));
if (firstVerifiedField) {
verification = <>· <VerifiedBadge link={firstVerifiedField.get('value')} verifiedAt={firstVerifiedField.get('verified_at')} /></>;
}
return (
<div className='account'>
<div className={classNames('account', { 'account--minimal': minimal })}>
<div className='account__wrapper'>
<Link key={account.get('id')} className='account__display-name' title={account.get('acct')} to={`/@${account.get('acct')}`}>
<div className='account__avatar-wrapper'><Avatar account={account} size={size} /></div>
{mute_expires_at}
<DisplayName account={account} />
<div className='account__avatar-wrapper'>
<Avatar account={account} size={size} />
</div>
<div>
<DisplayName account={account} />
{!minimal && <><ShortNumber value={account.get('followers_count')} renderer={counterRenderer('followers')} /> {verification} {muteTimeRemaining}</>}
</div>
</Link>
<div>
{children}
</div>
<div className='account__relationship'>
{buttons}
</div>
{!minimal && (
<div>
<div>
{children}
</div>
<div className='account__relationship'>
{buttons}
</div>
</div>
)}
</div>
</div>
);
}
}
export default injectIntl(Account);

View file

@ -33,7 +33,7 @@ class Category extends React.PureComponent {
const { id, text, disabled, selected, children } = this.props;
return (
<div tabIndex='0' role='button' className={classNames('report-reason-selector__category', { selected, disabled })} onClick={this.handleClick}>
<div tabIndex={0} role='button' className={classNames('report-reason-selector__category', { selected, disabled })} onClick={this.handleClick}>
{selected && <input type='hidden' name='report[category]' value={id} />}
<div className='report-reason-selector__category__label'>
@ -74,7 +74,7 @@ class Rule extends React.PureComponent {
const { id, text, disabled, selected } = this.props;
return (
<div tabIndex='0' role='button' className={classNames('report-reason-selector__rule', { selected, disabled })} onClick={this.handleClick}>
<div tabIndex={0} role='button' className={classNames('report-reason-selector__rule', { selected, disabled })} onClick={this.handleClick}>
<span className={classNames('poll__input', { checkbox: true, active: selected, disabled })} />
{selected && <input type='hidden' name='report[rule_ids][]' value={id} />}
{text}
@ -84,7 +84,6 @@ class Rule extends React.PureComponent {
}
export default @injectIntl
class ReportReasonSelector extends React.PureComponent {
static propTypes = {
@ -157,3 +156,5 @@ class ReportReasonSelector extends React.PureComponent {
}
}
export default injectIntl(ReportReasonSelector);

View file

@ -180,7 +180,7 @@ export default class AutosuggestInput extends ImmutablePureComponent {
}
return (
<div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
{inner}
</div>
);

View file

@ -186,7 +186,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
}
return (
<div role='button' tabIndex='0' key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
{inner}
</div>
);

View file

@ -1,62 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from '../initial_state';
import classNames from 'classnames';
export default class Avatar extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map,
size: PropTypes.number.isRequired,
style: PropTypes.object,
inline: PropTypes.bool,
animate: PropTypes.bool,
};
static defaultProps = {
animate: autoPlayGif,
size: 20,
inline: false,
};
state = {
hovering: false,
};
handleMouseEnter = () => {
if (this.props.animate) return;
this.setState({ hovering: true });
};
handleMouseLeave = () => {
if (this.props.animate) return;
this.setState({ hovering: false });
};
render () {
const { account, size, animate, inline } = this.props;
const { hovering } = this.state;
const style = {
...this.props.style,
width: `${size}px`,
height: `${size}px`,
};
let src;
if (hovering || animate) {
src = account?.get('avatar');
} else {
src = account?.get('avatar_static');
}
return (
<div className={classNames('account__avatar', { 'account__avatar-inline': inline })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} style={style}>
{src && <img src={src} alt={account?.get('acct')} />}
</div>
);
}
}

View file

@ -0,0 +1,49 @@
import * as React from 'react';
import classNames from 'classnames';
import { autoPlayGif } from '../initial_state';
import { useHovering } from '../../hooks/useHovering';
import type { Account } from '../../types/resources';
type Props = {
account: Account;
size: number;
style?: React.CSSProperties;
inline?: boolean;
animate?: boolean;
};
export const Avatar: React.FC<Props> = ({
account,
animate = autoPlayGif,
size = 20,
inline = false,
style: styleFromParent,
}) => {
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
const style = {
...styleFromParent,
width: `${size}px`,
height: `${size}px`,
};
const src =
hovering || animate
? account?.get('avatar')
: account?.get('avatar_static');
return (
<div
className={classNames('account__avatar', {
'account__avatar-inline': inline,
})}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={style}
>
{src && <img src={src} alt={account?.get('acct')} />}
</div>
);
};
export default Avatar;

View file

@ -44,6 +44,7 @@ function Blurhash({
const ctx = canvas.getContext('2d');
const imageData = new ImageData(pixels, width, height);
// @ts-expect-error
ctx.putImageData(imageData, 0, 0);
} catch (err) {
console.error('Blurhash decoding failure', { err, hash });

View file

@ -8,7 +8,7 @@ export default class ColumnBackButtonSlim extends ColumnBackButton {
render () {
return (
<div className='column-back-button--slim'>
<div role='button' tabIndex='0' onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
<div role='button' tabIndex={0} onClick={this.handleClick} className='column-back-button column-back-button--slim-button'>
<Icon id='chevron-left' className='column-back-button__icon' fixedWidth />
<FormattedMessage id='column_back_button.label' defaultMessage='Back' />
</div>

View file

@ -12,7 +12,6 @@ const messages = defineMessages({
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
});
export default @injectIntl
class ColumnHeader extends React.PureComponent {
static contextTypes = {
@ -209,3 +208,5 @@ class ColumnHeader extends React.PureComponent {
}
}
export default injectIntl(ColumnHeader);

View file

@ -8,7 +8,6 @@ const messages = defineMessages({
dismiss: { id: 'dismissable_banner.dismiss', defaultMessage: 'Dismiss' },
});
export default @injectIntl
class DismissableBanner extends React.PureComponent {
static propTypes = {
@ -49,3 +48,5 @@ class DismissableBanner extends React.PureComponent {
}
}
export default injectIntl(DismissableBanner);

View file

@ -8,7 +8,6 @@ const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
});
export default @injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
@ -40,3 +39,5 @@ class Account extends ImmutablePureComponent {
}
}
export default injectIntl(Account);

View file

@ -119,7 +119,7 @@ class DropdownMenu extends React.PureComponent {
return (
<li className='dropdown-menu__item' key={`${text}-${i}`}>
<a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
<a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex={0} ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}>
{text}
</a>
</li>

View file

@ -16,8 +16,6 @@ const mapDispatchToProps = (dispatch, { statusId }) => ({
});
export default @connect(null, mapDispatchToProps)
@injectIntl
class EditedTimestamp extends React.PureComponent {
static propTypes = {
@ -68,3 +66,5 @@ class EditedTimestamp extends React.PureComponent {
}
}
export default connect(null, mapDispatchToProps)(injectIntl(EditedTimestamp));

View file

@ -46,7 +46,7 @@ export default class GIFV extends React.PureComponent {
width={width}
height={height}
role='button'
tabIndex='0'
tabIndex={0}
aria-label={alt}
title={alt}
lang={lang}
@ -57,7 +57,7 @@ export default class GIFV extends React.PureComponent {
<video
src={src}
role='button'
tabIndex='0'
tabIndex={0}
aria-label={alt}
title={alt}
lang={lang}

View file

@ -5,7 +5,9 @@ import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Link } from 'react-router-dom';
// @ts-expect-error
import ShortNumber from 'mastodon/components/short_number';
// @ts-expect-error
import Skeleton from 'mastodon/components/skeleton';
import classNames from 'classnames';
@ -19,11 +21,11 @@ class SilentErrorBoundary extends React.Component {
error: false,
};
componentDidCatch () {
componentDidCatch() {
this.setState({ error: true });
}
render () {
render() {
if (this.state.error) {
return null;
}
@ -50,11 +52,13 @@ export const accountsCountRenderer = (displayNumber, pluralReady) => (
/>
);
// @ts-expect-error
export const ImmutableHashtag = ({ hashtag }) => (
<Hashtag
name={hashtag.get('name')}
to={`/tags/${hashtag.get('name')}`}
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
// @ts-expect-error
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
/>
);
@ -63,6 +67,7 @@ ImmutableHashtag.propTypes = {
hashtag: ImmutablePropTypes.map.isRequired,
};
// @ts-expect-error
const Hashtag = ({ name, to, people, uses, history, className, description, withGraph }) => (
<div className={classNames('trends__item', className)}>
<div className='trends__item__name'>

View file

@ -23,7 +23,7 @@ export default class IconButton extends React.PureComponent {
inverted: PropTypes.bool,
animate: PropTypes.bool,
overlay: PropTypes.bool,
tabIndex: PropTypes.string,
tabIndex: PropTypes.number,
counter: PropTypes.number,
obfuscateCount: PropTypes.bool,
href: PropTypes.string,
@ -36,7 +36,7 @@ export default class IconButton extends React.PureComponent {
disabled: false,
animate: false,
overlay: false,
tabIndex: '0',
tabIndex: 0,
ariaHidden: false,
};

View file

@ -14,7 +14,6 @@ const makeMapStateToProps = () => {
return mapStateToProps;
};
export default @connect(makeMapStateToProps)
class InlineAccount extends React.PureComponent {
static propTypes = {
@ -32,3 +31,5 @@ class InlineAccount extends React.PureComponent {
}
}
export default connect(makeMapStateToProps)(InlineAccount);

View file

@ -113,7 +113,7 @@ export default class IntersectionObserverArticle extends React.Component {
aria-setsize={listLength}
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
data-id={id}
tabIndex='0'
tabIndex={0}
>
{children && React.cloneElement(children, { hidden: true })}
</article>
@ -121,7 +121,7 @@ export default class IntersectionObserverArticle extends React.Component {
}
return (
<article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex='0'>
<article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex={0}>
{children && React.cloneElement(children, { hidden: false })}
</article>
);

View file

@ -7,7 +7,6 @@ const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
});
export default @injectIntl
class LoadGap extends React.PureComponent {
static propTypes = {
@ -32,3 +31,5 @@ class LoadGap extends React.PureComponent {
}
}
export default injectIntl(LoadGap);

View file

@ -248,7 +248,6 @@ class Item extends React.PureComponent {
}
export default @injectIntl
class MediaGallery extends React.PureComponent {
static propTypes = {
@ -396,3 +395,5 @@ class MediaGallery extends React.PureComponent {
}
}
export default injectIntl(MediaGallery);

View file

@ -1,29 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import illustration from 'mastodon/../images/elephant_ui_disappointed.svg';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
const MissingIndicator = ({ fullPage }) => (
<div className={classNames('regeneration-indicator', { 'regeneration-indicator--without-header': fullPage })}>
<div className='regeneration-indicator__figure'>
<img src={illustration} alt='' />
</div>
<div className='regeneration-indicator__label'>
<FormattedMessage id='missing_indicator.label' tagName='strong' defaultMessage='Not found' />
<FormattedMessage id='missing_indicator.sublabel' defaultMessage='This resource could not be found' />
</div>
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</div>
);
MissingIndicator.propTypes = {
fullPage: PropTypes.bool,
};
export default MissingIndicator;

View file

@ -15,7 +15,6 @@ const DefaultNavigation = () => (
</>
);
export default @withRouter
class NavigationPortal extends React.PureComponent {
render () {
@ -33,3 +32,4 @@ class NavigationPortal extends React.PureComponent {
}
}
export default withRouter(NavigationPortal);

View file

@ -6,7 +6,6 @@ import { connect } from 'react-redux';
import { debounce } from 'lodash';
import { FormattedMessage } from 'react-intl';
export default @connect()
class PictureInPicturePlaceholder extends React.PureComponent {
static propTypes = {
@ -59,7 +58,7 @@ class PictureInPicturePlaceholder extends React.PureComponent {
const { height } = this.state;
return (
<div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex='0' onClick={this.handleClick}>
<div ref={this.setRef} className='picture-in-picture-placeholder' style={{ height }} role='button' tabIndex={0} onClick={this.handleClick}>
<Icon id='window-restore' />
<FormattedMessage id='picture_in_picture.restore' defaultMessage='Put it back' />
</div>
@ -67,3 +66,5 @@ class PictureInPicturePlaceholder extends React.PureComponent {
}
}
export default connect()(PictureInPicturePlaceholder);

View file

@ -31,7 +31,6 @@ const makeEmojiMap = record => record.get('emojis').reduce((obj, emoji) => {
return obj;
}, {});
export default @injectIntl
class Poll extends ImmutablePureComponent {
static contextTypes = {
@ -155,7 +154,7 @@ class Poll extends ImmutablePureComponent {
{!showResults && (
<span
className={classNames('poll__input', { checkbox: poll.get('multiple'), active })}
tabIndex='0'
tabIndex={0}
role={poll.get('multiple') ? 'checkbox' : 'radio'}
onKeyPress={this.handleOptionKeyPress}
aria-checked={active}
@ -234,3 +233,5 @@ class Poll extends ImmutablePureComponent {
}
}
export default injectIntl(Poll);

View file

@ -121,7 +121,6 @@ const timeRemainingString = (intl, date, now, timeGiven = true) => {
return relativeTime;
};
export default @injectIntl
class RelativeTimestamp extends React.Component {
static propTypes = {
@ -197,3 +196,5 @@ class RelativeTimestamp extends React.Component {
}
}
export default injectIntl(RelativeTimestamp);

View file

@ -20,7 +20,6 @@ const mapStateToProps = (state, { scrollKey }) => {
};
};
export default @connect(mapStateToProps, null, null, { forwardRef: true })
class ScrollableList extends PureComponent {
static contextTypes = {
@ -365,3 +364,5 @@ class ScrollableList extends PureComponent {
}
}
export default connect(mapStateToProps, null, null, { forwardRef: true })(ScrollableList);

View file

@ -18,8 +18,6 @@ const mapStateToProps = state => ({
server: state.getIn(['server', 'server']),
});
export default @connect(mapStateToProps)
@injectIntl
class ServerBanner extends React.PureComponent {
static propTypes = {
@ -61,7 +59,7 @@ class ServerBanner extends React.PureComponent {
<div className='server-banner__meta__column'>
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} />
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
</div>
<div className='server-banner__meta__column'>
@ -91,3 +89,5 @@ class ServerBanner extends React.PureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(ServerBanner));

View file

@ -61,7 +61,6 @@ const messages = defineMessages({
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
});
export default @injectIntl
class Status extends ImmutablePureComponent {
static contextTypes = {
@ -343,7 +342,7 @@ class Status extends ImmutablePureComponent {
if (hidden) {
return (
<HotKeys handlers={handlers}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex='0'>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex={0}>
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
<span>{status.get('content')}</span>
</div>
@ -360,7 +359,7 @@ class Status extends ImmutablePureComponent {
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex={0} ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />: {matchedFilters.join(', ')}.
{' '}
<button className='status__wrapper--filtered__button' onClick={this.handleUnfilterClick}>
@ -392,6 +391,13 @@ class Status extends ImmutablePureComponent {
account = status.get('account');
status = status.get('reblog');
} else if (status.get('visibility') === 'direct') {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='at' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' />
</div>
);
} else if (showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id'])) {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
@ -549,7 +555,7 @@ class Status extends ImmutablePureComponent {
expanded={!status.get('hidden')}
onExpandedToggle={this.handleExpandedToggle}
onTranslate={this.handleTranslate}
collapsable
collapsible
onCollapsedToggle={this.handleCollapsedToggle}
/>
@ -565,3 +571,5 @@ class Status extends ImmutablePureComponent {
}
}
export default injectIntl(Status);

View file

@ -15,7 +15,7 @@ const messages = defineMessages({
delete: { id: 'status.delete', defaultMessage: 'Delete' },
redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' },
edit: { id: 'status.edit', defaultMessage: 'Edit' },
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
direct: { id: 'status.direct', defaultMessage: 'Privately mention @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
@ -55,8 +55,6 @@ const mapStateToProps = (state, { status }) => ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
});
export default @connect(mapStateToProps)
@injectIntl
class StatusActionBar extends ImmutablePureComponent {
static contextTypes = {
@ -405,3 +403,5 @@ class StatusActionBar extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(StatusActionBar));

View file

@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import classnames from 'classnames';
import PollContainer from 'mastodon/containers/poll_container';
import Icon from 'mastodon/components/icon';
@ -47,7 +48,10 @@ class TranslateButton extends React.PureComponent {
}
export default @injectIntl
const mapStateToProps = state => ({
languages: state.getIn(['server', 'translationLanguages', 'items']),
});
class StatusContent extends React.PureComponent {
static contextTypes = {
@ -61,8 +65,9 @@ class StatusContent extends React.PureComponent {
onExpandedToggle: PropTypes.func,
onTranslate: PropTypes.func,
onClick: PropTypes.func,
collapsable: PropTypes.bool,
collapsible: PropTypes.bool,
onCollapsedToggle: PropTypes.func,
languages: ImmutablePropTypes.map,
intl: PropTypes.object,
};
@ -107,10 +112,10 @@ class StatusContent extends React.PureComponent {
}
if (status.get('collapsed', null) === null && onCollapsedToggle) {
const { collapsable, onClick } = this.props;
const { collapsible, onClick } = this.props;
const collapsed =
collapsable
collapsible
&& onClick
&& node.clientHeight > MAX_HEIGHT
&& status.get('spoiler_text').length === 0;
@ -220,7 +225,9 @@ class StatusContent extends React.PureComponent {
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed');
const renderTranslate = this.props.onTranslate && status.get('translatable');
const contentLocale = intl.locale.replace(/[_-].*/, '');
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
const renderTranslate = this.props.onTranslate && this.context.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('contentHtml').length > 0 && targetLanguages?.includes(contentLocale);
const content = { __html: status.get('translation') ? status.getIn(['translation', 'content']) : status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') };
@ -261,7 +268,7 @@ class StatusContent extends React.PureComponent {
}
return (
<div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<p style={{ marginBottom: hidden && status.get('mentions').isEmpty() ? '0px' : null }}>
<span dangerouslySetInnerHTML={spoilerContent} className='translate' lang={lang} />
{' '}
@ -279,7 +286,7 @@ class StatusContent extends React.PureComponent {
} else if (this.props.onClick) {
return (
<>
<div className={classNames} ref={this.setRef} tabIndex='0' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className='status__content__text status__content__text--visible translate' lang={lang} dangerouslySetInnerHTML={content} />
{poll}
@ -291,7 +298,7 @@ class StatusContent extends React.PureComponent {
);
} else {
return (
<div className={classNames} ref={this.setRef} tabIndex='0' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className={classNames} ref={this.setRef} tabIndex={0} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
<div className='status__content__text status__content__text--visible translate' lang={lang} dangerouslySetInnerHTML={content} />
{poll}
@ -302,3 +309,5 @@ class StatusContent extends React.PureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(StatusContent));

View file

@ -2,7 +2,6 @@ import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { injectIntl } from 'react-intl';
import emojify from '../features/emoji/emoji';
import classNames from 'classnames';
import EmojiView from './emoji_view';
@ -50,7 +49,6 @@ class EmojiReactionButton extends React.PureComponent {
}
export default @injectIntl
class StatusEmojiReactionsBar extends React.PureComponent {
static propTypes = {
@ -73,7 +71,7 @@ class StatusEmojiReactionsBar extends React.PureComponent {
render () {
const { emojiReactions } = this.props;
const emojiButtons = Array.from(emojiReactions).filter(emoji => emoji.get('count') != 0).map((emoji, index) => (
const emojiButtons = Array.from(emojiReactions).filter(emoji => emoji.get('count') !== 0).map((emoji, index) => (
<EmojiReactionButton
key={index}
name={emoji.get('name')}
@ -94,3 +92,5 @@ class StatusEmojiReactionsBar extends React.PureComponent {
}
}
export default injectIntl(StatusEmojiReactionsBar);

View file

@ -54,7 +54,7 @@ export default class MediaContainer extends PureComponent {
handleCloseMedia = () => {
document.body.classList.remove('with-modals--active');
document.documentElement.style.marginRight = 0;
document.documentElement.style.marginRight = '0';
this.setState({
media: null,

View file

@ -67,7 +67,7 @@ class Section extends React.PureComponent {
return (
<div className={classNames('about__section', { active: !collapsed })}>
<div className='about__section__title' role='button' tabIndex='0' onClick={this.handleClick}>
<div className='about__section__title' role='button' tabIndex={0} onClick={this.handleClick}>
<Icon id={collapsed ? 'chevron-right' : 'chevron-down'} fixedWidth /> {title}
</div>
@ -80,8 +80,6 @@ class Section extends React.PureComponent {
}
export default @connect(mapStateToProps)
@injectIntl
class About extends React.PureComponent {
static propTypes = {
@ -125,7 +123,7 @@ class About extends React.PureComponent {
<div className='about__meta__column'>
<h4><FormattedMessage id='server_banner.administered_by' defaultMessage='Administered by:' /></h4>
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} />
<Account id={server.getIn(['contact', 'account', 'id'])} size={36} minimal />
</div>
<hr className='about__meta__divider' />
@ -217,3 +215,5 @@ class About extends React.PureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(About));

View file

@ -43,7 +43,6 @@ class InlineAlert extends React.PureComponent {
}
export default @injectIntl
class AccountNote extends ImmutablePureComponent {
static propTypes = {
@ -168,3 +167,5 @@ class AccountNote extends ImmutablePureComponent {
}
}
export default injectIntl(AccountNote);

View file

@ -10,7 +10,6 @@ const messages = defineMessages({
empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' },
});
export default @injectIntl
class FeaturedTags extends ImmutablePureComponent {
static contextTypes = {
@ -50,3 +49,5 @@ class FeaturedTags extends ImmutablePureComponent {
}
}
export default injectIntl(FeaturedTags);

View file

@ -28,7 +28,7 @@ const messages = defineMessages({
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' },
direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
@ -76,7 +76,6 @@ const dateFormatOptions = {
minute: '2-digit',
};
export default @injectIntl
class Header extends ImmutablePureComponent {
static contextTypes = {
@ -419,3 +418,5 @@ class Header extends ImmutablePureComponent {
}
}
export default injectIntl(Header);

View file

@ -19,7 +19,6 @@ const mapStateToProps = (state, { match: { params: { acct } } }) => {
};
};
export default @connect(mapStateToProps)
class AccountNavigation extends React.PureComponent {
static propTypes = {
@ -50,3 +49,5 @@ class AccountNavigation extends React.PureComponent {
}
}
export default connect(mapStateToProps)(AccountNavigation);

View file

@ -74,7 +74,7 @@ export default class MediaItem extends ImmutablePureComponent {
if (['audio', 'video'].includes(attachment.get('type'))) {
content = (
<img
src={attachment.get('preview_url') || attachment.getIn(['account', 'avatar_static'])}
src={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
alt={attachment.get('description')}
lang={status.get('language')}
onLoad={this.handleImageLoad}

View file

@ -13,10 +13,10 @@ import MediaItem from './components/media_item';
import HeaderContainer from '../account_timeline/containers/header_container';
import ScrollContainer from 'mastodon/containers/scroll_container';
import LoadMore from 'mastodon/components/load_more';
import MissingIndicator from 'mastodon/components/missing_indicator';
import { openModal } from 'mastodon/actions/modal';
import { FormattedMessage } from 'react-intl';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
const mapStateToProps = (state, { params: { acct, id } }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
@ -60,7 +60,6 @@ class LoadMoreMedia extends ImmutablePureComponent {
}
export default @connect(mapStateToProps)
class AccountGallery extends ImmutablePureComponent {
static propTypes = {
@ -162,9 +161,7 @@ class AccountGallery extends ImmutablePureComponent {
if (!isAccount) {
return (
<Column>
<MissingIndicator />
</Column>
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
);
}
@ -226,3 +223,5 @@ class AccountGallery extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(AccountGallery);

View file

@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import InnerHeader from '../../account/components/header';
import ImmutablePureComponent from 'react-immutable-pure-component';
import MemorialNote from './memorial_note';
import MovedNote from './moved_note';
import { FormattedMessage } from 'react-intl';
import { NavLink } from 'react-router-dom';
@ -115,6 +116,7 @@ export default class Header extends ImmutablePureComponent {
return (
<div className='account-timeline__header'>
{(!hidden && account.get('memorial')) && <MemorialNote />}
{(!hidden && account.get('moved')) && <MovedNote from={account} to={account.get('moved')} />}
<InnerHeader

View file

@ -14,7 +14,6 @@ const mapDispatchToProps = (dispatch, { accountId }) => ({
});
export default @connect(() => {}, mapDispatchToProps)
class LimitedAccountHint extends React.PureComponent {
static propTypes = {
@ -34,3 +33,5 @@ class LimitedAccountHint extends React.PureComponent {
}
}
export default connect(() => {}, mapDispatchToProps)(LimitedAccountHint);

View file

@ -0,0 +1,12 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
const MemorialNote = () => (
<div className='account-memorial-banner'>
<div className='account-memorial-banner__message'>
<FormattedMessage id='account.in_memoriam' defaultMessage='In Memoriam.' />
</div>
</div>
);
export default MemorialNote;

View file

@ -12,7 +12,6 @@ import ColumnBackButton from '../../components/column_back_button';
import { List as ImmutableList } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { FormattedMessage } from 'react-intl';
import MissingIndicator from 'mastodon/components/missing_indicator';
import TimelineHint from 'mastodon/components/timeline_hint';
import { me } from 'mastodon/initial_state';
import { connectTimeline, disconnectTimeline } from 'mastodon/actions/timelines';
@ -20,6 +19,7 @@ import LimitedAccountHint from './components/limited_account_hint';
import { getAccountHidden } from 'mastodon/selectors';
import { fetchFeaturedTags } from '../../actions/featured_tags';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
const emptyList = ImmutableList();
@ -64,7 +64,6 @@ RemoteHint.propTypes = {
url: PropTypes.string.isRequired,
};
export default @connect(mapStateToProps)
class AccountTimeline extends ImmutablePureComponent {
static propTypes = {
@ -158,10 +157,7 @@ class AccountTimeline extends ImmutablePureComponent {
);
} else if (!isLoading && !isAccount) {
return (
<Column>
<ColumnBackButton multiColumn={multiColumn} />
<MissingIndicator />
</Column>
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
);
}
@ -206,3 +202,5 @@ class AccountTimeline extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(AccountTimeline);

View file

@ -22,7 +22,6 @@ const messages = defineMessages({
const TICK_SIZE = 10;
const PADDING = 180;
export default @injectIntl
class Audio extends React.PureComponent {
static propTypes = {
@ -471,7 +470,7 @@ class Audio extends React.PureComponent {
}
return (
<div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex='0' onKeyDown={this.handleKeyDown}>
<div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>
<Blurhash
hash={blurhash}
@ -494,7 +493,7 @@ class Audio extends React.PureComponent {
<canvas
role='button'
tabIndex='0'
tabIndex={0}
className='audio-player__canvas'
width={this.state.width}
height={this.state.height}
@ -527,7 +526,7 @@ class Audio extends React.PureComponent {
<span
className={classNames('video-player__seek__handle', { active: dragging })}
tabIndex='0'
tabIndex={0}
style={{ left: `${progress}%`, backgroundColor: this._getAccentColor() }}
onKeyDown={this.handleAudioKeyDown}
/>
@ -544,7 +543,7 @@ class Audio extends React.PureComponent {
<span
className='video-player__volume__handle'
tabIndex='0'
tabIndex={0}
style={{ left: `${volume * 100}%`, backgroundColor: this._getAccentColor() }}
/>
</div>
@ -569,3 +568,5 @@ class Audio extends React.PureComponent {
}
}
export default injectIntl(Audio);

View file

@ -22,8 +22,6 @@ const mapStateToProps = state => ({
isLoading: state.getIn(['user_lists', 'blocks', 'isLoading'], true),
});
export default @connect(mapStateToProps)
@injectIntl
class Blocks extends ImmutablePureComponent {
static propTypes = {
@ -77,3 +75,5 @@ class Blocks extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(Blocks));

View file

@ -22,8 +22,6 @@ const mapStateToProps = state => ({
hasMore: !!state.getIn(['status_lists', 'bookmarks', 'next']),
});
export default @connect(mapStateToProps)
@injectIntl
class Bookmarks extends ImmutablePureComponent {
static propTypes = {
@ -106,3 +104,5 @@ class Bookmarks extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(Bookmarks));

View file

@ -9,7 +9,6 @@ const mapStateToProps = state => ({
message: state.getIn(['server', 'server', 'registrations', 'message']),
});
export default @connect(mapStateToProps)
class ClosedRegistrationsModal extends ImmutablePureComponent {
componentDidMount () {
@ -73,3 +72,5 @@ class ClosedRegistrationsModal extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(ClosedRegistrationsModal);

View file

@ -4,7 +4,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { injectIntl, FormattedMessage } from 'react-intl';
import SettingToggle from '../../notifications/components/setting_toggle';
export default @injectIntl
class ColumnSettings extends React.PureComponent {
static propTypes = {
@ -27,3 +26,5 @@ class ColumnSettings extends React.PureComponent {
}
}
export default injectIntl(ColumnSettings);

View file

@ -30,8 +30,6 @@ const mapStateToProps = (state, { columnId }) => {
};
};
export default @connect(mapStateToProps)
@injectIntl
class CommunityTimeline extends React.PureComponent {
static contextTypes = {
@ -158,3 +156,5 @@ class CommunityTimeline extends React.PureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(CommunityTimeline));

View file

@ -21,7 +21,6 @@ const messages = defineMessages({
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
});
export default @injectIntl
class ActionBar extends React.PureComponent {
static propTypes = {
@ -67,3 +66,5 @@ class ActionBar extends React.PureComponent {
}
}
export default injectIntl(ActionBar);

View file

@ -34,7 +34,6 @@ const messages = defineMessages({
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
});
export default @injectIntl
class ComposeForm extends ImmutablePureComponent {
static contextTypes = {
@ -212,7 +211,6 @@ class ComposeForm extends ImmutablePureComponent {
};
handleExpirationPick = (data) => {
const { text } = this.props;
const position = this.autosuggestTextarea.textarea.selectionStart;
this.props.onPickExpiration(position, data);
@ -314,3 +312,5 @@ class ComposeForm extends ImmutablePureComponent {
}
}
export default injectIntl(ComposeForm);

View file

@ -144,8 +144,7 @@ class ModifierPicker extends React.PureComponent {
}
@injectIntl
class EmojiPickerMenu extends React.PureComponent {
class EmojiPickerMenuImpl extends React.PureComponent {
static propTypes = {
custom_emojis: ImmutablePropTypes.list,
@ -305,7 +304,8 @@ class EmojiPickerMenu extends React.PureComponent {
}
export default @injectIntl
const EmojiPickerMenu = injectIntl(EmojiPickerMenuImpl);
class EmojiPickerDropdown extends React.PureComponent {
static propTypes = {
@ -428,3 +428,5 @@ class EmojiPickerDropdown extends React.PureComponent {
}
}
export default injectIntl(EmojiPickerDropdown);

View file

@ -118,7 +118,6 @@ class ExpirationDropdownMenu extends React.PureComponent {
}
export default @injectIntl
class ExpirationDropdown extends React.PureComponent {
static propTypes = {
@ -265,3 +264,5 @@ class ExpirationDropdown extends React.PureComponent {
}
}
export default injectIntl(ExpirationDropdown);

View file

@ -209,7 +209,7 @@ class LanguageDropdownMenu extends React.PureComponent {
const { value } = this.props;
return (
<div key={lang[0]} role='option' tabIndex='0' data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={this.handleClick} onKeyDown={this.handleKeyDown}>
<div key={lang[0]} role='option' tabIndex={0} data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={this.handleClick} onKeyDown={this.handleKeyDown}>
<span className='language-dropdown__dropdown__results__item__native-name' lang={lang[0]}>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span>
</div>
);
@ -237,7 +237,6 @@ class LanguageDropdownMenu extends React.PureComponent {
}
export default @injectIntl
class LanguageDropdown extends React.PureComponent {
static propTypes = {
@ -325,3 +324,5 @@ class LanguageDropdown extends React.PureComponent {
}
}
export default injectIntl(LanguageDropdown);

View file

@ -13,8 +13,6 @@ const iconStyle = {
lineHeight: '27px',
};
export default
@injectIntl
class PollButton extends React.PureComponent {
static propTypes = {
@ -53,3 +51,5 @@ class PollButton extends React.PureComponent {
}
}
export default injectIntl(PollButton);

View file

@ -20,8 +20,7 @@ const messages = defineMessages({
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
});
@injectIntl
class Option extends React.PureComponent {
class OptionIntl extends React.PureComponent {
static propTypes = {
title: PropTypes.string.isRequired,
@ -83,7 +82,7 @@ class Option extends React.PureComponent {
onClick={this.handleToggleMultiple}
onKeyPress={this.handleCheckboxKeypress}
role='button'
tabIndex='0'
tabIndex={0}
title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
/>
@ -113,8 +112,8 @@ class Option extends React.PureComponent {
}
export default
@injectIntl
const Option = injectIntl(OptionIntl);
class PollForm extends ImmutablePureComponent {
static propTypes = {
@ -180,3 +179,5 @@ class PollForm extends ImmutablePureComponent {
}
}
export default injectIntl(PollForm);

View file

@ -117,7 +117,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
return (
<div style={{ ...style }} role='listbox' ref={this.setRef}>
{items.map(item => (
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
<div role='option' tabIndex={0} key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
<div className='privacy-dropdown__option__icon'>
<Icon id={item.icon} fixedWidth />
</div>
@ -134,7 +134,6 @@ class PrivacyDropdownMenu extends React.PureComponent {
}
export default @injectIntl
class PrivacyDropdown extends React.PureComponent {
static propTypes = {
@ -288,3 +287,5 @@ class PrivacyDropdown extends React.PureComponent {
}
}
export default injectIntl(PrivacyDropdown);

View file

@ -12,7 +12,6 @@ const messages = defineMessages({
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
});
export default @injectIntl
class ReplyIndicator extends ImmutablePureComponent {
static contextTypes = {
@ -69,3 +68,5 @@ class ReplyIndicator extends ImmutablePureComponent {
}
}
export default injectIntl(ReplyIndicator);

View file

@ -1,38 +1,17 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Overlay from 'react-overlays/Overlay';
import { searchEnabled } from '../../../initial_state';
import { searchEnabled } from 'mastodon/initial_state';
import Icon from 'mastodon/components/icon';
import classNames from 'classnames';
import { HASHTAG_REGEX } from 'mastodon/utils/hashtags';
const messages = defineMessages({
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' },
});
class SearchPopout extends React.PureComponent {
render () {
const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />;
return (
<div className='search-popout'>
<h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4>
<ul>
<li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li>
<li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
<li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li>
<li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li>
</ul>
{extraInformation}
</div>
);
}
}
export default @injectIntl
class Search extends React.PureComponent {
static contextTypes = {
@ -42,9 +21,13 @@ class Search extends React.PureComponent {
static propTypes = {
value: PropTypes.string.isRequired,
recent: ImmutablePropTypes.orderedSet,
submitted: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onOpenURL: PropTypes.func.isRequired,
onClickSearchResult: PropTypes.func.isRequired,
onForgetSearchResult: PropTypes.func.isRequired,
onClear: PropTypes.func.isRequired,
onShow: PropTypes.func.isRequired,
openInRoute: PropTypes.bool,
@ -54,44 +37,94 @@ class Search extends React.PureComponent {
state = {
expanded: false,
selectedOption: -1,
options: [],
};
setRef = c => {
this.searchForm = c;
};
handleChange = (e) => {
this.props.onChange(e.target.value);
handleChange = ({ target }) => {
const { onChange } = this.props;
onChange(target.value);
this._calculateOptions(target.value);
};
handleClear = (e) => {
handleClear = e => {
const { value, submitted, onClear } = this.props;
e.preventDefault();
if (this.props.value.length > 0 || this.props.submitted) {
this.props.onClear();
if (value.length > 0 || submitted) {
onClear();
this.setState({ options: [], selectedOption: -1 });
}
};
handleKeyUp = (e) => {
if (e.key === 'Enter') {
handleKeyDown = (e) => {
const { selectedOption } = this.state;
const options = this._getOptions();
switch(e.key) {
case 'Escape':
e.preventDefault();
this._unfocus();
break;
case 'ArrowDown':
e.preventDefault();
this.props.onSubmit();
if (this.props.openInRoute) {
this.context.router.history.push('/search');
if (options.length > 0) {
this.setState({ selectedOption: Math.min(selectedOption + 1, options.length - 1) });
}
} else if (e.key === 'Escape') {
document.querySelector('.ui').parentElement.focus();
break;
case 'ArrowUp':
e.preventDefault();
if (options.length > 0) {
this.setState({ selectedOption: Math.max(selectedOption - 1, -1) });
}
break;
case 'Enter':
e.preventDefault();
if (selectedOption === -1) {
this._submit();
} else if (options.length > 0) {
options[selectedOption].action();
}
this._unfocus();
break;
case 'Delete':
if (selectedOption > -1 && options.length > 0) {
const search = options[selectedOption];
if (typeof search.forget === 'function') {
e.preventDefault();
search.forget(e);
}
}
break;
}
};
handleFocus = () => {
this.setState({ expanded: true });
this.props.onShow();
const { onShow, singleColumn } = this.props;
if (this.searchForm && !this.props.singleColumn) {
this.setState({ expanded: true, selectedOption: -1 });
onShow();
if (this.searchForm && !singleColumn) {
const { left, right } = this.searchForm.getBoundingClientRect();
if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) {
this.searchForm.scrollIntoView();
}
@ -99,21 +132,148 @@ class Search extends React.PureComponent {
};
handleBlur = () => {
this.setState({ expanded: false });
this.setState({ expanded: false, selectedOption: -1 });
};
findTarget = () => {
return this.searchForm;
};
handleHashtagClick = () => {
const { router } = this.context;
const { value, onClickSearchResult } = this.props;
const query = value.trim().replace(/^#/, '');
router.history.push(`/tags/${query}`);
onClickSearchResult(query, 'hashtag');
};
handleAccountClick = () => {
const { router } = this.context;
const { value, onClickSearchResult } = this.props;
const query = value.trim().replace(/^@/, '');
router.history.push(`/@${query}`);
onClickSearchResult(query, 'account');
};
handleURLClick = () => {
const { router } = this.context;
const { onOpenURL } = this.props;
onOpenURL(router.history);
};
handleStatusSearch = () => {
this._submit('statuses');
};
handleAccountSearch = () => {
this._submit('accounts');
};
handleRecentSearchClick = search => {
const { router } = this.context;
if (search.get('type') === 'account') {
router.history.push(`/@${search.get('q')}`);
} else if (search.get('type') === 'hashtag') {
router.history.push(`/tags/${search.get('q')}`);
}
};
handleForgetRecentSearchClick = search => {
const { onForgetSearchResult } = this.props;
onForgetSearchResult(search.get('q'));
};
_unfocus () {
document.querySelector('.ui').parentElement.focus();
}
_submit (type) {
const { onSubmit, openInRoute } = this.props;
const { router } = this.context;
onSubmit(type);
if (openInRoute) {
router.history.push('/search');
}
}
_getOptions () {
const { options } = this.state;
if (options.length > 0) {
return options;
}
const { recent } = this.props;
return recent.toArray().map(search => ({
label: search.get('type') === 'account' ? `@${search.get('q')}` : `#${search.get('q')}`,
action: () => this.handleRecentSearchClick(search),
forget: e => {
e.stopPropagation();
this.handleForgetRecentSearchClick(search);
},
}));
}
_calculateOptions (value) {
const trimmedValue = value.trim();
const options = [];
if (trimmedValue.length > 0) {
const couldBeURL = trimmedValue.startsWith('https://') && !trimmedValue.includes(' ');
if (couldBeURL) {
options.push({ key: 'open-url', label: <FormattedMessage id='search.quick_action.open_url' defaultMessage='Open URL in Mastodon' />, action: this.handleURLClick });
}
const couldBeHashtag = (trimmedValue.startsWith('#') && trimmedValue.length > 1) || trimmedValue.match(HASHTAG_REGEX);
if (couldBeHashtag) {
options.push({ key: 'go-to-hashtag', label: <FormattedMessage id='search.quick_action.go_to_hashtag' defaultMessage='Go to hashtag {x}' values={{ x: <mark>#{trimmedValue.replace(/^#/, '')}</mark> }} />, action: this.handleHashtagClick });
}
const couldBeUsername = trimmedValue.match(/^@?[a-z0-9_-]+(@[^\s]+)?$/i);
if (couldBeUsername) {
options.push({ key: 'go-to-account', label: <FormattedMessage id='search.quick_action.go_to_account' defaultMessage='Go to profile {x}' values={{ x: <mark>@{trimmedValue.replace(/^@/, '')}</mark> }} />, action: this.handleAccountClick });
}
const couldBeStatusSearch = searchEnabled;
if (couldBeStatusSearch) {
options.push({ key: 'status-search', label: <FormattedMessage id='search.quick_action.status_search' defaultMessage='Posts matching {x}' values={{ x: <mark>{trimmedValue}</mark> }} />, action: this.handleStatusSearch });
}
const couldBeUserSearch = true;
if (couldBeUserSearch) {
options.push({ key: 'account-search', label: <FormattedMessage id='search.quick_action.account_search' defaultMessage='Profiles matching {x}' values={{ x: <mark>{trimmedValue}</mark> }} />, action: this.handleAccountSearch });
}
}
this.setState({ options });
}
render () {
const { intl, value, submitted } = this.props;
const { expanded } = this.state;
const { intl, value, submitted, recent } = this.props;
const { expanded, options, selectedOption } = this.state;
const { signedIn } = this.context.identity;
const hasValue = value.length > 0 || submitted;
return (
<div className='search'>
<div className={classNames('search', { active: expanded })}>
<input
ref={this.setRef}
className='search__input'
@ -122,26 +282,54 @@ class Search extends React.PureComponent {
aria-label={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)}
value={value}
onChange={this.handleChange}
onKeyUp={this.handleKeyUp}
onKeyDown={this.handleKeyDown}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
/>
<div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}>
<div role='button' tabIndex={0} className='search__icon' onClick={this.handleClear}>
<Icon id='search' className={hasValue ? '' : 'active'} />
<Icon id='times-circle' className={hasValue ? 'active' : ''} aria-label={intl.formatMessage(messages.placeholder)} />
</div>
<Overlay show={expanded && !hasValue} placement='bottom' target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
{({ props, placement }) => (
<div {...props} style={{ ...props.style, width: 285, zIndex: 2 }}>
<div className={`dropdown-animation ${placement}`}>
<SearchPopout />
<div className='search__popout'>
{options.length === 0 && (
<>
<h4><FormattedMessage id='search_popout.recent' defaultMessage='Recent searches' /></h4>
<div className='search__popout__menu'>
{recent.size > 0 ? this._getOptions().map(({ label, action, forget }, i) => (
<button key={label} onMouseDown={action} className={classNames('search__popout__menu__item search__popout__menu__item--flex', { selected: selectedOption === i })}>
<span>{label}</span>
<button className='icon-button' onMouseDown={forget}><Icon id='times' /></button>
</button>
)) : (
<div className='search__popout__menu__message'>
<FormattedMessage id='search.no_recent_searches' defaultMessage='No recent searches' />
</div>
)}
</div>
</div>
</>
)}
</Overlay>
{options.length > 0 && (
<>
<h4><FormattedMessage id='search_popout.quick_actions' defaultMessage='Quick actions' /></h4>
<div className='search__popout__menu'>
{options.map(({ key, label, action }, i) => (
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === i })}>
{label}
</button>
))}
</div>
</>
)}
</div>
</div>
);
}
}
export default injectIntl(Search);

View file

@ -14,7 +14,6 @@ const messages = defineMessages({
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
});
export default @injectIntl
class SearchResults extends ImmutablePureComponent {
static propTypes = {
@ -82,7 +81,7 @@ class SearchResults extends ImmutablePureComponent {
const showMore = this.showMoreResults('accounts');
accounts = (
<div className='search-results__section'>
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='People' /></h5>
<h5><Icon id='users' fixedWidth /><FormattedMessage id='search_results.accounts' defaultMessage='Profiles' /></h5>
{results.get('accounts').map(accountId => <AccountContainer key={accountId} id={accountId} />)}
@ -144,3 +143,5 @@ class SearchResults extends ImmutablePureComponent {
}
}
export default injectIntl(SearchResults);

View file

@ -132,7 +132,6 @@ class SearchabilityDropdownMenu extends React.PureComponent {
}
export default @injectIntl
class SearchabilityDropdown extends React.PureComponent {
static propTypes = {
@ -284,3 +283,5 @@ class SearchabilityDropdown extends React.PureComponent {
}
}
export default injectIntl(SearchabilityDropdown);

View file

@ -42,7 +42,7 @@ export default class Upload extends ImmutablePureComponent {
const y = ((focusY / -2) + .5) * 100;
return (
<div className='compose-form__upload' tabIndex='0' role='button'>
<div className='compose-form__upload' tabIndex={0} role='button'>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => (
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>

View file

@ -23,8 +23,6 @@ const iconStyle = {
lineHeight: '27px',
};
export default @connect(makeMapStateToProps)
@injectIntl
class UploadButton extends ImmutablePureComponent {
static propTypes = {
@ -81,3 +79,5 @@ class UploadButton extends ImmutablePureComponent {
}
}
export default connect(makeMapStateToProps)(injectIntl(UploadButton));

View file

@ -4,12 +4,16 @@ import {
clearSearch,
submitSearch,
showSearch,
} from '../../../actions/search';
openURL,
clickSearchResult,
forgetSearchResult,
} from 'mastodon/actions/search';
import Search from '../components/search';
const mapStateToProps = state => ({
value: state.getIn(['search', 'value']),
submitted: state.getIn(['search', 'submitted']),
recent: state.getIn(['search', 'recent']),
});
const mapDispatchToProps = dispatch => ({
@ -22,14 +26,26 @@ const mapDispatchToProps = dispatch => ({
dispatch(clearSearch());
},
onSubmit () {
dispatch(submitSearch());
onSubmit (type) {
dispatch(submitSearch(type));
},
onShow () {
dispatch(showSearch());
},
onOpenURL (routerHistory) {
dispatch(openURL(routerHistory));
},
onClickSearchResult (q, type) {
dispatch(clickSearchResult(q, type));
},
onForgetSearchResult (q) {
dispatch(forgetSearchResult(q));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Search);

View file

@ -3,36 +3,12 @@ import { connect } from 'react-redux';
import Warning from '../components/warning';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { me } from '../../../initial_state';
const buildHashtagRE = () => {
try {
const HASHTAG_SEPARATORS = '_\\u00b7\\u200c';
const ALPHA = '\\p{L}\\p{M}';
const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
return new RegExp(
'(?:^|[^\\/\\)\\w])#((' +
'[' + WORD + '_]' +
'[' + WORD + HASHTAG_SEPARATORS + ']*' +
'[' + ALPHA + HASHTAG_SEPARATORS + ']' +
'[' + WORD + HASHTAG_SEPARATORS +']*' +
'[' + WORD + '_]' +
')|(' +
'[' + WORD + '_]*' +
'[' + ALPHA + ']' +
'[' + WORD + '_]*' +
'))', 'iu',
);
} catch {
return /(?:^|[^/)\w])#(\w*[a-zA-Z·]\w*)/i;
}
};
const APPROX_HASHTAG_RE = buildHashtagRE();
import { me } from 'mastodon/initial_state';
import { HASHTAG_PATTERN_REGEX } from 'mastodon/utils/hashtags';
const mapStateToProps = state => ({
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
hashtagWarning: ['public', 'public_unlisted'].indexOf(state.getIn(['compose', 'privacy'])) < 0 && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])),
hashtagWarning: ['public', 'public_unlisted'].indexOf(state.getIn(['compose', 'privacy'])) < 0 && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])),
directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
searchabilityWarning: state.getIn(['compose', 'searchability']) === 'direct',
});
@ -57,7 +33,7 @@ const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning
}
if (searchabilityWarning) {
return <Warning message={<FormattedMessage id='compose_form.searchability_warning' defaultMessage="Self only searchability is not available other mastodon servers. Others can search your post." />} />;
return <Warning message={<FormattedMessage id='compose_form.searchability_warning' defaultMessage='Self only searchability is not available other mastodon servers. Others can search your post.' />} />;
}
return null;
@ -67,6 +43,7 @@ WarningWrapper.propTypes = {
needsLockWarning: PropTypes.bool,
hashtagWarning: PropTypes.bool,
directMessageWarning: PropTypes.bool,
searchabilityWarning: PropTypes.bool,
};
export default connect(mapStateToProps)(WarningWrapper);

View file

@ -38,8 +38,6 @@ const mapStateToProps = (state, ownProps) => ({
showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : false,
});
export default @connect(mapStateToProps)
@injectIntl
class Compose extends React.PureComponent {
static propTypes = {
@ -148,3 +146,5 @@ class Compose extends React.PureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(Compose));

View file

@ -24,7 +24,6 @@ const messages = defineMessages({
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
});
export default @injectIntl
class Conversation extends ImmutablePureComponent {
static contextTypes = {
@ -145,7 +144,7 @@ class Conversation extends ImmutablePureComponent {
return (
<HotKeys handlers={handlers}>
<div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex='0'>
<div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex={0}>
<div className='conversation__avatar' onClick={this.handleClick} role='presentation'>
<AvatarComposite accounts={accounts} size={48} />
</div>
@ -166,7 +165,7 @@ class Conversation extends ImmutablePureComponent {
onClick={this.handleClick}
expanded={!lastStatus.get('hidden')}
onExpandedToggle={this.handleShowMore}
collapsable
collapsible
/>
{lastStatus.get('media_attachments').size > 0 && (
@ -198,3 +197,5 @@ class Conversation extends ImmutablePureComponent {
}
}
export default injectIntl(Conversation);

View file

@ -55,10 +55,10 @@ export default class ConversationsList extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { conversations, onLoadMore, ...other } = this.props;
const { conversations, isLoading, onLoadMore, ...other } = this.props;
return (
<ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
<ScrollableList {...other} isLoading={isLoading} showLoading={isLoading && conversations.isEmpty()} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
{conversations.map(item => (
<ConversationContainer
key={item.get('id')}

View file

@ -11,11 +11,9 @@ import ColumnHeader from 'mastodon/components/column_header';
import ConversationsListContainer from './containers/conversations_list_container';
const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
title: { id: 'column.direct', defaultMessage: 'Private mentions' },
});
export default @connect()
@injectIntl
class DirectTimeline extends React.PureComponent {
static propTypes = {
@ -91,9 +89,11 @@ class DirectTimeline extends React.PureComponent {
trackScroll={!pinned}
scrollKey={`direct_timeline-${columnId}`}
timelineId='direct'
bindToDocument={!multiColumn}
onLoadMore={this.handleLoadMore}
prepend={<div className='follow_requests-unlocked_explanation'><span><FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a></span></div>}
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
alwaysPrepend
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any private mentions yet. When you send or receive one, it will show up here." />}
/>
<Helmet>
@ -105,3 +105,5 @@ class DirectTimeline extends React.PureComponent {
}
}
export default connect()(injectIntl(DirectTimeline));

View file

@ -91,9 +91,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
});
export default
@injectIntl
@connect(makeMapStateToProps, mapDispatchToProps)
class AccountCard extends ImmutablePureComponent {
static propTypes = {
@ -233,3 +230,5 @@ class AccountCard extends ImmutablePureComponent {
}
}
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(AccountCard));

View file

@ -29,8 +29,6 @@ const mapStateToProps = state => ({
domain: state.getIn(['meta', 'domain']),
});
export default @connect(mapStateToProps)
@injectIntl
class Directory extends React.PureComponent {
static contextTypes = {
@ -176,3 +174,5 @@ class Directory extends React.PureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(Directory));

View file

@ -23,8 +23,6 @@ const mapStateToProps = state => ({
hasMore: !!state.getIn(['domain_lists', 'blocks', 'next']),
});
export default @connect(mapStateToProps)
@injectIntl
class Blocks extends ImmutablePureComponent {
static propTypes = {
@ -81,3 +79,5 @@ class Blocks extends ImmutablePureComponent {
}
}
export default connect(mapStateToProps)(injectIntl(Blocks));

View file

@ -50,7 +50,7 @@ const emojifyTextNode = (node, customEmojis) => {
if (shortname in customEmojis) {
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
replacement = document.createElement('img');
replacement.setAttribute('draggable', false);
replacement.setAttribute('draggable', 'false');
replacement.setAttribute('class', 'emojione custom-emoji');
replacement.setAttribute('alt', shortname);
replacement.setAttribute('title', shortname);
@ -65,7 +65,7 @@ const emojifyTextNode = (node, customEmojis) => {
const { filename, shortCode } = unicodeMapping[match];
const title = shortCode ? `:${shortCode}:` : '';
replacement = document.createElement('img');
replacement.setAttribute('draggable', false);
replacement.setAttribute('draggable', 'false');
replacement.setAttribute('class', 'emojione');
replacement.setAttribute('alt', match);
replacement.setAttribute('title', title);

View file

@ -9,7 +9,7 @@ const emojis = {};
// decompress
Object.keys(shortCodesToEmojiData).forEach((shortCode) => {
let [
filenameData, // eslint-disable-line no-unused-vars
filenameData, // eslint-disable-line @typescript-eslint/no-unused-vars
searchData,
] = shortCodesToEmojiData[shortCode];
let [

Some files were not shown because too many files have changed in this diff Show more