diff --git a/Gemfile b/Gemfile index 991ef0861d..ace4b58e6c 100644 --- a/Gemfile +++ b/Gemfile @@ -12,7 +12,7 @@ gem 'thor', '~> 0.20' gem 'hamlit-rails', '~> 0.2' gem 'pg', '~> 1.1' gem 'makara', '~> 0.4' -gem 'pghero', '~> 2.2' +gem 'pghero', '~> 2.3' gem 'dotenv-rails', '~> 2.7' gem 'aws-sdk-s3', '~> 1.46', require: false @@ -62,12 +62,12 @@ gem 'mime-types', '~> 3.2', require: 'mime/types/columnar' gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532' gem 'nokogiri', '~> 1.10' gem 'nsa', '~> 0.2' -gem 'oj', '~> 3.8' +gem 'oj', '~> 3.9' gem 'ostatus2', '~> 2.0' gem 'ox', '~> 2.11' gem 'parslet' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' -gem 'pundit', '~> 2.0' +gem 'pundit', '~> 2.1' gem 'premailer-rails' gem 'rack-attack', '~> 6.1' gem 'rack-cors', '~> 1.0', require: 'rack/cors' @@ -81,7 +81,7 @@ gem 'sidekiq', '~> 5.2' gem 'sidekiq-scheduler', '~> 3.0' gem 'sidekiq-unique-jobs', '~> 6.0' gem 'sidekiq-bulk', '~>0.2.0' -gem 'simple-navigation', '~> 4.0' +gem 'simple-navigation', '~> 4.1' gem 'simple_form', '~> 4.1' gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie' gem 'stoplight', '~> 2.1.3' @@ -134,7 +134,7 @@ group :development do gem 'letter_opener_web', '~> 1.3' gem 'memory_profiler' gem 'rubocop', '~> 0.74', require: false - gem 'rubocop-rails', '~> 2.2', require: false + gem 'rubocop-rails', '~> 2.3', require: false gem 'brakeman', '~> 4.6', require: false gem 'bundler-audit', '~> 0.6', require: false diff --git a/Gemfile.lock b/Gemfile.lock index b99330a256..0af2b2a890 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -387,7 +387,7 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.2) sidekiq (>= 3.5) statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.8.1) + oj (3.9.0) omniauth (1.9.0) hashie (>= 3.4.6, < 3.7.0) rack (>= 1.6.2, < 3) @@ -423,9 +423,9 @@ GEM equatable (~> 0.5.0) tty-color (~> 0.4.0) pg (1.1.4) - pghero (2.2.1) - activerecord - pkg-config (1.3.7) + pghero (2.3.0) + activerecord (>= 5) + pkg-config (1.3.8) premailer (1.11.1) addressable css_parser (>= 1.6.0) @@ -445,7 +445,7 @@ GEM public_suffix (3.1.1) puma (4.1.0) nio4r (~> 2.0) - pundit (2.0.1) + pundit (2.1.0) activesupport (>= 3.0.0) raabro (1.1.6) rack (2.0.7) @@ -555,7 +555,7 @@ GEM rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 1.7) - rubocop-rails (2.2.1) + rubocop-rails (2.3.0) rack (>= 1.1) rubocop (>= 0.72.0) ruby-progressbar (1.10.1) @@ -584,7 +584,7 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.5) sidekiq (>= 4.0, < 7.0) thor (~> 0) - simple-navigation (4.0.5) + simple-navigation (4.1.0) activesupport (>= 2.3.2) simple_form (4.1.0) actionpack (>= 5.0) @@ -732,7 +732,7 @@ DEPENDENCIES nilsimsa! nokogiri (~> 1.10) nsa (~> 0.2) - oj (~> 3.8) + oj (~> 3.9) omniauth (~> 1.9) omniauth-cas (~> 1.1) omniauth-saml (~> 1.10) @@ -743,7 +743,7 @@ DEPENDENCIES parallel_tests (~> 2.29) parslet pg (~> 1.1) - pghero (~> 2.2) + pghero (~> 2.3) pkg-config (~> 1.3) posix-spawn! premailer-rails @@ -751,7 +751,7 @@ DEPENDENCIES pry-byebug (~> 3.7) pry-rails (~> 0.3) puma (~> 4.1) - pundit (~> 2.0) + pundit (~> 2.1) rack-attack (~> 6.1) rack-cors (~> 1.0) rails (~> 5.2.3) @@ -767,13 +767,13 @@ DEPENDENCIES rspec-rails (~> 3.8) rspec-sidekiq (~> 3.0) rubocop (~> 0.74) - rubocop-rails (~> 2.2) + rubocop-rails (~> 2.3) sanitize (~> 5.0) sidekiq (~> 5.2) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 3.0) sidekiq-unique-jobs (~> 6.0) - simple-navigation (~> 4.0) + simple-navigation (~> 4.1) simple_form (~> 4.1) simplecov (~> 0.17) sprockets-rails (~> 3.2) diff --git a/app/chewy/accounts_index.rb b/app/chewy/accounts_index.rb new file mode 100644 index 0000000000..b814e009e5 --- /dev/null +++ b/app/chewy/accounts_index.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class AccountsIndex < Chewy::Index + settings index: { refresh_interval: '5m' }, analysis: { + analyzer: { + content: { + tokenizer: 'whitespace', + filter: %w(lowercase asciifolding cjk_width), + }, + + edge_ngram: { + tokenizer: 'edge_ngram', + filter: %w(lowercase asciifolding cjk_width), + }, + }, + + tokenizer: { + edge_ngram: { + type: 'edge_ngram', + min_gram: 1, + max_gram: 15, + }, + }, + } + + define_type ::Account.searchable.includes(:account_stat), delete_if: ->(account) { account.destroyed? || !account.searchable? } do + root date_detection: false do + field :id, type: 'long' + + field :display_name, type: 'text', analyzer: 'content' do + field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' + end + + field :acct, type: 'text', analyzer: 'content', value: ->(account) { [account.username, account.domain].compact.join('@') } do + field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' + end + + field :following_count, type: 'long', value: ->(account) { account.following.local.count } + field :followers_count, type: 'long', value: ->(account) { account.followers.local.count } + field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at } + end + end +end diff --git a/app/chewy/tags_index.rb b/app/chewy/tags_index.rb new file mode 100644 index 0000000000..300fc128f6 --- /dev/null +++ b/app/chewy/tags_index.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +class TagsIndex < Chewy::Index + settings index: { refresh_interval: '15m' }, analysis: { + analyzer: { + content: { + tokenizer: 'keyword', + filter: %w(lowercase asciifolding cjk_width), + }, + + edge_ngram: { + tokenizer: 'edge_ngram', + filter: %w(lowercase asciifolding cjk_width), + }, + }, + + tokenizer: { + edge_ngram: { + type: 'edge_ngram', + min_gram: 2, + max_gram: 15, + }, + }, + } + + define_type ::Tag.listable, delete_if: ->(tag) { tag.destroyed? || !tag.listable? } do + root date_detection: false do + field :name, type: 'text', analyzer: 'content' do + field :edge_ngram, type: 'text', analyzer: 'edge_ngram', search_analyzer: 'content' + end + + field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? } + field :usage, type: 'long', value: ->(tag) { tag.history.reduce(0) { |total, day| total + day[:accounts].to_i } } + field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at } + end + end +end diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index f41e52aae9..7b04381278 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -4,10 +4,12 @@ class AboutController < ApplicationController before_action :set_pack layout 'public' - before_action :require_open_federation!, only: [:show, :more] + before_action :require_open_federation!, only: [:show, :more, :blocks] + before_action :check_blocklist_enabled, only: [:blocks] + before_action :authenticate_user!, only: [:blocks], if: :blocklist_account_required? before_action :set_body_classes, only: :show before_action :set_instance_presenter - before_action :set_expires_in + before_action :set_expires_in, only: [:show, :more, :terms] skip_before_action :require_functional!, only: [:more, :terms] @@ -19,12 +21,40 @@ class AboutController < ApplicationController def terms; end + def blocks + @show_rationale = Setting.show_domain_blocks_rationale == 'all' + @show_rationale |= Setting.show_domain_blocks_rationale == 'users' && !current_user.nil? && current_user.functional? + @blocks = DomainBlock.with_user_facing_limitations.order('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), reject_media, domain').to_a + end + private def require_open_federation! not_found if whitelist_mode? end + def check_blocklist_enabled + not_found if Setting.show_domain_blocks == 'disabled' + end + + def blocklist_account_required? + Setting.show_domain_blocks == 'users' + end + + def block_severity_text(block) + if block.severity == 'suspend' + I18n.t('domain_blocks.suspension') + else + limitations = [] + limitations << I18n.t('domain_blocks.media_block') if block.reject_media? + limitations << I18n.t('domain_blocks.silence') if block.severity == 'silence' + limitations.join(', ') + end + end + + helper_method :block_severity_text + helper_method :public_fetch_mode? + def new_user User.new.tap do |user| user.build_account @@ -35,7 +65,7 @@ class AboutController < ApplicationController helper_method :new_user def set_pack - use_pack 'common' + use_pack 'public' end def set_instance_presenter diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 1a876b831b..817e5e832d 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -19,6 +19,7 @@ class AccountsController < ApplicationController @pinned_statuses = [] @endorsed_accounts = @account.endorsed_accounts.to_a.sample(4) + @featured_hashtags = @account.featured_tags.order(statuses_count: :desc) if current_account && @account.blocking?(current_account) @statuses = [] @@ -28,6 +29,7 @@ class AccountsController < ApplicationController @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses? @statuses = filtered_status_page(params) @statuses = cache_collection(@statuses, Status) + @rss_url = rss_url unless @statuses.empty? @older_url = older_url if @statuses.last.id > filtered_statuses.last.id @@ -38,8 +40,9 @@ class AccountsController < ApplicationController format.rss do expires_in 0, public: true - @statuses = cache_collection(default_statuses.without_reblogs.without_replies.limit(PAGE_SIZE), Status) - render xml: RSS::AccountSerializer.render(@account, @statuses) + @statuses = filtered_statuses.without_reblogs.without_replies.limit(PAGE_SIZE) + @statuses = cache_collection(@statuses, Status) + render xml: RSS::AccountSerializer.render(@account, @statuses, params[:tag]) end format.json do @@ -97,6 +100,14 @@ class AccountsController < ApplicationController params[:username] end + def rss_url + if tag_requested? + short_account_tag_url(@account, params[:tag], format: 'rss') + else + short_account_url(@account, format: 'rss') + end + end + def older_url pagination_url(max_id: @statuses.last.id) end @@ -126,7 +137,7 @@ class AccountsController < ApplicationController end def tag_requested? - request.path.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) + request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) end def filtered_status_page(params) diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index ab755ed4e6..c62061555b 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -27,7 +27,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController end def set_replies - @replies = page_params[:other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses + @replies = page_params[:only_other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) end @@ -38,7 +38,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController type: :unordered, part_of: account_status_replies_url(@account, @status), next: next_page, - items: @replies.map { |status| status.local ? status : status.id } + items: @replies.map { |status| status.local ? status : status.uri } ) return page if page_requested? @@ -55,16 +55,17 @@ class ActivityPub::RepliesController < ActivityPub::BaseController end def next_page + only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT) account_status_replies_url( @account, @status, page: true, - min_id: @replies&.last&.id, - other_accounts: !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT) + min_id: only_other_accounts && !page_params[:only_other_accounts] ? nil : @replies&.last&.id, + only_other_accounts: only_other_accounts ) end def page_params - params_slice(:other_accounts, :min_id).merge(page: true) + params_slice(:only_other_accounts, :min_id).merge(page: true) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 19efc8838b..5f88838e41 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -26,10 +26,13 @@ class ApplicationController < ActionController::Base rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity rescue_from ActionController::UnknownFormat, with: :not_acceptable rescue_from Mastodon::NotPermittedError, with: :forbidden + rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error before_action :store_current_location, except: :raise_not_found, unless: :devise_controller? before_action :require_functional!, if: :user_signed_in? + skip_before_action :verify_authenticity_token, only: :raise_not_found + def raise_not_found raise ActionController::RoutingError, "No route matches #{params[:unmatched_route]}" end @@ -163,6 +166,10 @@ class ApplicationController < ActionController::Base respond_with_error(406) end + def internal_server_error + respond_with_error(500) + end + def single_user_mode? @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists? end diff --git a/app/controllers/concerns/signature_verification.rb b/app/controllers/concerns/signature_verification.rb index 7b251cf804..ce353f1dee 100644 --- a/app/controllers/concerns/signature_verification.rb +++ b/app/controllers/concerns/signature_verification.rb @@ -23,6 +23,19 @@ module SignatureVerification @signature_verification_failure_code || 401 end + def signature_key_id + raw_signature = request.headers['Signature'] + signature_params = {} + + raw_signature.split(',').each do |part| + parsed_parts = part.match(/([a-z]+)="([^"]+)"/i) + next if parsed_parts.nil? || parsed_parts.size != 3 + signature_params[parsed_parts[1]] = parsed_parts[2] + end + + signature_params['keyId'] + end + def signed_request_account return @signed_request_account if defined?(@signed_request_account) @@ -154,7 +167,7 @@ module SignatureVerification .with_fallback { nil } .with_threshold(1) .with_cool_off_time(5.minutes.seconds) - .with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) } + .with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) } .run end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index e2ba9bf001..4641a8bb92 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -7,6 +7,8 @@ class FollowerAccountsController < ApplicationController before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers + skip_around_action :set_locale, if: -> { request.format == :json } + def index respond_to do |format| format.html do diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 49f1f32189..6e80554fba 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -7,6 +7,8 @@ class FollowingAccountsController < ApplicationController before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? } before_action :set_cache_headers + skip_around_action :set_locale, if: -> { request.format == :json } + def index respond_to do |format| format.html do diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index a09aed8014..efdb1d2268 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -5,7 +5,6 @@ class HomeController < ApplicationController before_action :set_pack before_action :set_referrer_policy_header - before_action :set_initial_state_json def index @body_classes = 'app-body' @@ -45,21 +44,6 @@ class HomeController < ApplicationController use_pack 'home' end - def set_initial_state_json - serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) - @initial_state_json = serializable_resource.to_json - end - - def initial_state_params - { - settings: Web::Setting.find_by(user: current_user)&.data || {}, - push_subscription: current_account.user.web_push_subscription(current_session), - current_account: current_account, - token: current_session.token, - admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')), - } - end - def default_redirect_path if request.path.start_with?('/web') || whitelist_mode? new_user_session_path diff --git a/app/controllers/instance_actors_controller.rb b/app/controllers/instance_actors_controller.rb index 41f33602e6..6f02d6a358 100644 --- a/app/controllers/instance_actors_controller.rb +++ b/app/controllers/instance_actors_controller.rb @@ -3,6 +3,8 @@ class InstanceActorsController < ApplicationController include AccountControllerConcern + skip_around_action :set_locale + def show expires_in 10.minutes, public: true render json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter, fields: restrict_fields_to diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 639002964e..0b3c082dce 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -48,7 +48,7 @@ class InvitesController < ApplicationController end def resource_params - params.require(:invite).permit(:max_uses, :expires_in, :autofollow) + params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment) end def set_body_classes diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 8da6c6fe0c..558cd6e301 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -7,6 +7,8 @@ class MediaProxyController < ApplicationController before_action :authenticate_user!, if: :whitelist_mode? + rescue_from ActiveRecord::RecordInvalid, with: :not_found + def show RedisLock.acquire(lock_options) do |lock| if lock.acquired? diff --git a/app/controllers/public_timelines_controller.rb b/app/controllers/public_timelines_controller.rb index 940b2f7cd1..eb5bb191b9 100644 --- a/app/controllers/public_timelines_controller.rb +++ b/app/controllers/public_timelines_controller.rb @@ -9,12 +9,7 @@ class PublicTimelinesController < ApplicationController before_action :set_body_classes before_action :set_instance_presenter - def show - @initial_state_json = ActiveModelSerializers::SerializableResource.new( - InitialStatePresenter.new(settings: { known_fediverse: Setting.show_known_fediverse_at_about_page }, token: current_session&.token), - serializer: InitialStateSerializer - ).to_json - end + def show; end private diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb index ada4eec545..e13e7e8b65 100644 --- a/app/controllers/shares_controller.rb +++ b/app/controllers/shares_controller.rb @@ -7,26 +7,10 @@ class SharesController < ApplicationController before_action :set_pack before_action :set_body_classes - def show - serializable_resource = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(initial_state_params), serializer: InitialStateSerializer) - @initial_state_json = serializable_resource.to_json - end + def show; end private - def initial_state_params - text = [params[:title], params[:text], params[:url]].compact.join(' ') - - { - settings: Web::Setting.find_by(user: current_user)&.data || {}, - push_subscription: current_account.user.web_push_subscription(current_session), - current_account: current_account, - token: current_session.token, - admin: Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')), - text: text, - } - end - def set_pack use_pack 'share' end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index d6bb28eb51..c447a3a2b8 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -18,11 +18,6 @@ class TagsController < ApplicationController format.html do use_pack 'about' expires_in 0, public: true - - @initial_state_json = ActiveModelSerializers::SerializableResource.new( - InitialStatePresenter.new(settings: {}, token: current_session&.token), - serializer: InitialStateSerializer - ).to_json end format.rss do diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7ae1e5d0ba..6940c85350 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -123,4 +123,25 @@ module ApplicationHelper text = word_wrap(text, line_width: line_width - 2, break_sequence: break_sequence) text.split("\n").map { |line| '> ' + line }.join("\n") end + + def render_initial_state + state_params = { + settings: { + known_fediverse: Setting.show_known_fediverse_at_about_page, + }, + + text: [params[:title], params[:text], params[:url]].compact.join(' '), + } + + if user_signed_in? + state_params[:settings] = state_params[:settings].merge(Web::Setting.find_by(user: current_user)&.data || {}) + state_params[:push_subscription] = current_account.user.web_push_subscription(current_session) + state_params[:current_account] = current_account + state_params[:token] = current_session.token + state_params[:admin] = Account.find_local(Setting.site_contact_username.strip.gsub(/\A@/, '')) + end + + json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json + content_tag(:script, json_escape(json).html_safe, id: 'initial-state', type: 'application/json') + end end diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 170efad043..88994c2ac6 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -486,13 +486,30 @@ class Status extends ImmutablePureComponent { return null; } + const handlers = { + reply: this.handleHotkeyReply, + favourite: this.handleHotkeyFavourite, + boost: this.handleHotkeyBoost, + mention: this.handleHotkeyMention, + open: this.handleHotkeyOpen, + openProfile: this.handleHotkeyOpenProfile, + moveUp: this.handleHotkeyMoveUp, + moveDown: this.handleHotkeyMoveDown, + toggleSpoiler: this.handleExpandedToggle, + bookmark: this.handleHotkeyBookmark, + toggleCollapse: this.handleHotkeyCollapse, + toggleSensitive: this.handleHotkeyToggleSensitive, + }; + if (hidden) { return ( -
- {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} - {' '} - {status.get('content')} -
+ +
+ {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} + {' '} + {status.get('content')} +
+
); } @@ -628,21 +645,6 @@ class Status extends ImmutablePureComponent { rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: account.get('acct') }); } - const handlers = { - reply: this.handleHotkeyReply, - favourite: this.handleHotkeyFavourite, - boost: this.handleHotkeyBoost, - mention: this.handleHotkeyMention, - open: this.handleHotkeyOpen, - openProfile: this.handleHotkeyOpenProfile, - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - toggleSpoiler: this.handleExpandedToggle, - bookmark: this.handleHotkeyBookmark, - toggleCollapse: this.handleHotkeyCollapse, - toggleSensitive: this.handleHotkeyToggleSensitive, - }; - const computedClass = classNames('status', `status-${status.get('visibility')}`, { collapsed: isCollapsed, 'has-background': isCollapsed && background, diff --git a/app/javascript/flavours/glitch/features/compose/components/character_counter.js b/app/javascript/flavours/glitch/features/compose/components/character_counter.js new file mode 100644 index 0000000000..0ecfc9141d --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/character_counter.js @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { length } from 'stringz'; + +export default class CharacterCounter extends React.PureComponent { + + static propTypes = { + text: PropTypes.string.isRequired, + max: PropTypes.number.isRequired, + }; + + checkRemainingText (diff) { + if (diff < 0) { + return {diff}; + } + + return {diff}; + } + + render () { + const diff = this.props.max - length(this.props.text); + return this.checkRemainingText(diff); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index 3d9002fe48..6e07998ec2 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -15,6 +15,8 @@ import { countableText } from 'flavours/glitch/util/counter'; import OptionsContainer from '../containers/options_container'; import Publisher from './publisher'; import TextareaIcons from './textarea_icons'; +import { maxChars } from 'flavours/glitch/util/initial_state'; +import CharacterCounter from './character_counter'; const messages = defineMessages({ placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, @@ -119,14 +121,8 @@ class ComposeForm extends ImmutablePureComponent { // Submit unless there are media with missing descriptions if (mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media.some(item => !item.get('description'))) { - const firstWithoutDescription = media.findIndex(item => !item.get('description')); - if (uploadForm) { - const inputs = uploadForm.querySelectorAll('.composer--upload_form--item input'); - if (inputs.length == media.size && firstWithoutDescription !== -1) { - inputs[firstWithoutDescription].focus(); - } - } - onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null); + const firstWithoutDescription = media.find(item => !item.get('description')); + onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null, firstWithoutDescription.get('id')); } else if (onSubmit) { onSubmit(this.context.router ? this.context.router.history : null); } @@ -298,6 +294,8 @@ class ComposeForm extends ImmutablePureComponent { let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia); + const countText = `${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`; + return (
@@ -347,19 +345,24 @@ class ComposeForm extends ImmutablePureComponent {
- 0)} - spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler} - /> +
+ 0)} + spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler} + /> +
+ +
+
- {diff} {sideArm && sideArm !== 'none' ? ( - {media.get('type') === 'image' && } - - -
-