Merge remote-tracking branch 'parent/main' into upstream-20240126
|
@ -165,7 +165,7 @@ module.exports = defineConfig({
|
|||
// },
|
||||
// ],
|
||||
'jsx-a11y/no-noninteractive-tabindex': 'off',
|
||||
'jsx-a11y/no-onchange': 'warn',
|
||||
'jsx-a11y/no-onchange': 'off',
|
||||
// recommended is full 'error'
|
||||
'jsx-a11y/no-static-element-interactions': [
|
||||
'warn',
|
||||
|
|
19
.github/workflows/test-migrations-one-step.yml
vendored
|
@ -78,23 +78,8 @@ jobs:
|
|||
- name: Create database
|
||||
run: './bin/rails db:create'
|
||||
|
||||
- name: Run migrations up to v2.0.0
|
||||
run: './bin/rails db:migrate VERSION=20171010025614'
|
||||
|
||||
- name: Populate database with test data
|
||||
run: './bin/rails tests:migrations:populate_v2'
|
||||
|
||||
- name: Run migrations up to v2.4.0
|
||||
run: './bin/rails db:migrate VERSION=20180514140000'
|
||||
|
||||
- name: Populate database with test data
|
||||
run: './bin/rails tests:migrations:populate_v2_4'
|
||||
|
||||
- name: Run migrations up to v2.4.3
|
||||
run: './bin/rails db:migrate VERSION=20180707154237'
|
||||
|
||||
- name: Populate database with test data
|
||||
run: './bin/rails tests:migrations:populate_v2_4_3'
|
||||
- name: Run historical migrations with data population
|
||||
run: './bin/rails tests:migrations:prepare_database'
|
||||
|
||||
- name: Run all remaining migrations
|
||||
run: './bin/rails db:migrate'
|
||||
|
|
22
.github/workflows/test-migrations-two-step.yml
vendored
|
@ -45,6 +45,7 @@ jobs:
|
|||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
options: >-
|
||||
|
@ -77,28 +78,11 @@ jobs:
|
|||
- name: Create database
|
||||
run: './bin/rails db:create'
|
||||
|
||||
- name: Run migrations up to v2.0.0
|
||||
run: './bin/rails db:migrate VERSION=20171010025614'
|
||||
|
||||
- name: Populate database with test data
|
||||
run: './bin/rails tests:migrations:populate_v2'
|
||||
|
||||
- name: Run pre-deployment migrations up to v2.4.0
|
||||
run: './bin/rails db:migrate VERSION=20180514140000'
|
||||
- name: Run historical migrations with data population
|
||||
run: './bin/rails tests:migrations:prepare_database'
|
||||
env:
|
||||
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||
|
||||
- name: Populate database with test data
|
||||
run: './bin/rails tests:migrations:populate_v2_4'
|
||||
|
||||
- name: Run migrations up to v2.4.3
|
||||
run: './bin/rails db:migrate VERSION=20180707154237'
|
||||
env:
|
||||
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
|
||||
|
||||
- name: Populate database with test data
|
||||
run: './bin/rails tests:migrations:populate_v2_4_3'
|
||||
|
||||
- name: Run all remaining pre-deployment migrations
|
||||
run: './bin/rails db:migrate'
|
||||
env:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp`
|
||||
# using RuboCop version 1.59.0.
|
||||
# using RuboCop version 1.60.2.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
|
@ -73,21 +73,6 @@ Rails/UniqueValidationWithoutIndex:
|
|||
- 'app/models/identity.rb'
|
||||
- 'app/models/webauthn_credential.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: exists, where
|
||||
Rails/WhereExists:
|
||||
Exclude:
|
||||
- 'app/controllers/activitypub/inboxes_controller.rb'
|
||||
- 'app/controllers/admin/email_domain_blocks_controller.rb'
|
||||
- 'app/policies/status_policy.rb'
|
||||
- 'app/serializers/rest/announcement_serializer.rb'
|
||||
- 'app/workers/move_worker.rb'
|
||||
- 'spec/models/account_spec.rb'
|
||||
- 'spec/services/activitypub/process_collection_service_spec.rb'
|
||||
- 'spec/services/purge_domain_service_spec.rb'
|
||||
- 'spec/services/unallow_domain_service_spec.rb'
|
||||
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||
# AllowedMethods: ==, equal?, eql?
|
||||
|
@ -139,10 +124,6 @@ Style/GlobalStdStream:
|
|||
# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
|
||||
Style/GuardClause:
|
||||
Exclude:
|
||||
- 'app/controllers/admin/confirmations_controller.rb'
|
||||
- 'app/controllers/auth/confirmations_controller.rb'
|
||||
- 'app/controllers/auth/passwords_controller.rb'
|
||||
- 'app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb'
|
||||
- 'app/lib/activitypub/activity/block.rb'
|
||||
- 'app/lib/request.rb'
|
||||
- 'app/lib/request_pool.rb'
|
||||
|
@ -298,13 +279,6 @@ Style/StringLiterals:
|
|||
- 'config/routes.rb'
|
||||
- 'db/schema.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
|
||||
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
|
||||
Style/TernaryParentheses:
|
||||
Exclude:
|
||||
- 'config/environments/development.rb'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: EnforcedStyleForMultiline.
|
||||
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
|
||||
|
|
2
Gemfile
|
@ -123,7 +123,7 @@ group :test do
|
|||
gem 'database_cleaner-active_record'
|
||||
|
||||
# Used to mock environment variables
|
||||
gem 'climate_control', '~> 0.2'
|
||||
gem 'climate_control'
|
||||
|
||||
# Generating fake data for specs
|
||||
gem 'faker', '~> 3.2'
|
||||
|
|
14
Gemfile.lock
|
@ -185,7 +185,7 @@ GEM
|
|||
elasticsearch (>= 7.12.0, < 7.14.0)
|
||||
elasticsearch-dsl
|
||||
chunky_png (1.4.0)
|
||||
climate_control (0.2.0)
|
||||
climate_control (1.2.0)
|
||||
cocoon (1.2.15)
|
||||
color_diff (0.1)
|
||||
concurrent-ruby (1.2.3)
|
||||
|
@ -360,7 +360,7 @@ GEM
|
|||
rainbow (>= 2.2.2, < 4.0)
|
||||
terminal-table (>= 1.5.1)
|
||||
idn-ruby (0.1.5)
|
||||
io-console (0.7.1)
|
||||
io-console (0.7.2)
|
||||
irb (1.11.1)
|
||||
rdoc
|
||||
reline (>= 0.4.2)
|
||||
|
@ -445,7 +445,7 @@ GEM
|
|||
mime-types-data (3.2023.1205)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.5)
|
||||
minitest (5.21.1)
|
||||
minitest (5.21.2)
|
||||
msgpack (1.7.2)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.3.0)
|
||||
|
@ -636,7 +636,7 @@ GEM
|
|||
rspec-mocks (3.12.6)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-rails (6.1.0)
|
||||
rspec-rails (6.1.1)
|
||||
actionpack (>= 6.1)
|
||||
activesupport (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
|
@ -748,8 +748,8 @@ GEM
|
|||
temple (0.10.3)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
terrapin (0.6.0)
|
||||
climate_control (>= 0.0.3, < 1.0)
|
||||
terrapin (1.0.1)
|
||||
climate_control
|
||||
test-prof (1.3.1)
|
||||
thor (1.3.0)
|
||||
tilt (2.3.0)
|
||||
|
@ -838,7 +838,7 @@ DEPENDENCIES
|
|||
capybara (~> 3.39)
|
||||
charlock_holmes (~> 0.7.7)
|
||||
chewy (~> 7.3)
|
||||
climate_control (~> 0.2)
|
||||
climate_control
|
||||
cocoon (~> 1.2)
|
||||
color_diff (~> 0.1)
|
||||
concurrent-ruby
|
||||
|
|
|
@ -24,7 +24,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
|
|||
|
||||
def unknown_affected_account?
|
||||
json = Oj.load(body, mode: :strict)
|
||||
json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.where(uri: json['actor']).exists?
|
||||
json.is_a?(Hash) && %w(Delete Update).include?(json['type']) && json['actor'].present? && json['actor'] == value_or_id(json['object']) && !Account.exists?(uri: json['actor'])
|
||||
rescue Oj::ParseError
|
||||
false
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
module Admin
|
||||
class ConfirmationsController < BaseController
|
||||
before_action :set_user
|
||||
before_action :check_confirmation, only: [:resend]
|
||||
before_action :redirect_confirmed_user, only: [:resend], if: :user_confirmed?
|
||||
|
||||
def create
|
||||
authorize @user, :confirm?
|
||||
|
@ -25,11 +25,13 @@ module Admin
|
|||
|
||||
private
|
||||
|
||||
def check_confirmation
|
||||
if @user.confirmed?
|
||||
flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed')
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
def redirect_confirmed_user
|
||||
flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed')
|
||||
redirect_to admin_accounts_path
|
||||
end
|
||||
|
||||
def user_confirmed?
|
||||
@user.confirmed?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,7 +38,7 @@ module Admin
|
|||
log_action :create, @email_domain_block
|
||||
|
||||
(@email_domain_block.other_domains || []).uniq.each do |domain|
|
||||
next if EmailDomainBlock.where(domain: domain).exists?
|
||||
next if EmailDomainBlock.exists?(domain: domain)
|
||||
|
||||
other_email_domain_block = EmailDomainBlock.create!(domain: domain, allow_with_approval: @email_domain_block.allow_with_approval, parent: @email_domain_block)
|
||||
log_action :create, other_email_domain_block
|
||||
|
|
|
@ -49,7 +49,7 @@ module Admin
|
|||
next
|
||||
end
|
||||
|
||||
@warning_domains = Instance.where(domain: @domain_blocks.map(&:domain)).where('EXISTS (SELECT 1 FROM follows JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id WHERE accounts.domain = instances.domain)').pluck(:domain)
|
||||
@warning_domains = instances_from_imported_blocks.pluck(:domain)
|
||||
rescue ActionController::ParameterMissing
|
||||
flash.now[:alert] = I18n.t('admin.export_domain_blocks.no_file')
|
||||
set_dummy_import!
|
||||
|
@ -58,6 +58,10 @@ module Admin
|
|||
|
||||
private
|
||||
|
||||
def instances_from_imported_blocks
|
||||
Instance.with_domain_follows(@domain_blocks.map(&:domain))
|
||||
end
|
||||
|
||||
def export_filename
|
||||
'domain_blocks.csv'
|
||||
end
|
||||
|
|
|
@ -7,7 +7,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
|||
|
||||
before_action :set_body_classes
|
||||
before_action :set_confirmation_user!, only: [:show, :confirm_captcha]
|
||||
before_action :require_unconfirmed!
|
||||
before_action :redirect_confirmed_user, if: :signed_in_confirmed_user?
|
||||
|
||||
before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha]
|
||||
before_action :require_captcha_if_needed!, only: [:show]
|
||||
|
@ -65,10 +65,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
|
|||
@confirmation_user.nil? || @confirmation_user.confirmed?
|
||||
end
|
||||
|
||||
def require_unconfirmed!
|
||||
if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
|
||||
redirect_to(current_user.approved? ? root_path : edit_user_registration_path)
|
||||
end
|
||||
def redirect_confirmed_user
|
||||
redirect_to(current_user.approved? ? root_path : edit_user_registration_path)
|
||||
end
|
||||
|
||||
def signed_in_confirmed_user?
|
||||
user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
class Auth::PasswordsController < Devise::PasswordsController
|
||||
skip_before_action :check_self_destruct!
|
||||
before_action :check_validity_of_reset_password_token, only: :edit
|
||||
before_action :redirect_invalid_reset_token, only: :edit, unless: :reset_password_token_is_valid?
|
||||
before_action :set_body_classes
|
||||
|
||||
layout 'auth'
|
||||
|
@ -19,11 +19,9 @@ class Auth::PasswordsController < Devise::PasswordsController
|
|||
|
||||
private
|
||||
|
||||
def check_validity_of_reset_password_token
|
||||
unless reset_password_token_is_valid?
|
||||
flash[:error] = I18n.t('auth.invalid_reset_password_token')
|
||||
redirect_to new_password_path(resource_name)
|
||||
end
|
||||
def redirect_invalid_reset_token
|
||||
flash[:error] = I18n.t('auth.invalid_reset_password_token')
|
||||
redirect_to new_password_path(resource_name)
|
||||
end
|
||||
|
||||
def set_body_classes
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Redirect::AccountsController < ApplicationController
|
||||
class Redirect::AccountsController < Redirect::BaseController
|
||||
private
|
||||
|
||||
def set_resource
|
||||
|
|
|
@ -6,8 +6,8 @@ module Settings
|
|||
skip_before_action :check_self_destruct!
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :require_otp_enabled
|
||||
before_action :require_webauthn_enabled, only: [:index, :destroy]
|
||||
before_action :redirect_invalid_otp, unless: -> { current_user.otp_enabled? }
|
||||
before_action :redirect_invalid_webauthn, only: [:index, :destroy], unless: -> { current_user.webauthn_enabled? }
|
||||
|
||||
def index; end
|
||||
def new; end
|
||||
|
@ -85,18 +85,14 @@ module Settings
|
|||
|
||||
private
|
||||
|
||||
def require_otp_enabled
|
||||
unless current_user.otp_enabled?
|
||||
flash[:error] = t('webauthn_credentials.otp_required')
|
||||
redirect_to settings_two_factor_authentication_methods_path
|
||||
end
|
||||
def redirect_invalid_otp
|
||||
flash[:error] = t('webauthn_credentials.otp_required')
|
||||
redirect_to settings_two_factor_authentication_methods_path
|
||||
end
|
||||
|
||||
def require_webauthn_enabled
|
||||
unless current_user.webauthn_enabled?
|
||||
flash[:error] = t('webauthn_credentials.not_enabled')
|
||||
redirect_to settings_two_factor_authentication_methods_path
|
||||
end
|
||||
def redirect_invalid_webauthn
|
||||
flash[:error] = t('webauthn_credentials.not_enabled')
|
||||
redirect_to settings_two_factor_authentication_methods_path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
25
app/javascript/images/warning-stripes.svg
Executable file
|
@ -0,0 +1,25 @@
|
|||
<svg width="5" height="80" viewBox="0 0 5 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_253_1286)">
|
||||
<rect width="5" height="80" fill="url(#paint0_linear_253_1286)"/>
|
||||
<line x1="-0.860365" y1="6.80136" x2="10.6078" y2="-1.22871" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="14.8314" x2="10.6078" y2="6.80132" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="22.8615" x2="10.6078" y2="14.8314" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="30.8916" x2="10.6078" y2="22.8615" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="38.9216" x2="10.6078" y2="30.8915" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="46.9517" x2="10.6078" y2="38.9216" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="54.9818" x2="10.6078" y2="46.9517" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="63.0118" x2="10.6078" y2="54.9817" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="71.0419" x2="10.6078" y2="63.0118" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="79.072" x2="10.6078" y2="71.0419" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="87.102" x2="10.6078" y2="79.072" stroke="black" stroke-width="3"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_253_1286" x1="2.5" y1="0" x2="2.5" y2="80" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEC84B"/>
|
||||
<stop offset="1" stop-color="#F79009"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_253_1286">
|
||||
<rect width="5" height="80" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -9,7 +9,11 @@ exports[`<AutosuggestEmoji /> renders emoji with custom url 1`] = `
|
|||
className="emojione"
|
||||
src="http://example.com/emoji.png"
|
||||
/>
|
||||
:foobar:
|
||||
<div
|
||||
className="autosuggest-emoji__name"
|
||||
>
|
||||
:foobar:
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
@ -22,6 +26,10 @@ exports[`<AutosuggestEmoji /> renders native emoji 1`] = `
|
|||
className="emojione"
|
||||
src="/emoji/1f499.svg"
|
||||
/>
|
||||
:foobar:
|
||||
<div
|
||||
className="autosuggest-emoji__name"
|
||||
>
|
||||
:foobar:
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
|
@ -37,10 +37,10 @@ class Account extends ImmutablePureComponent {
|
|||
static propTypes = {
|
||||
size: PropTypes.number,
|
||||
account: ImmutablePropTypes.record,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
onMuteNotifications: PropTypes.func.isRequired,
|
||||
onFollow: PropTypes.func,
|
||||
onBlock: PropTypes.func,
|
||||
onMute: PropTypes.func,
|
||||
onMuteNotifications: PropTypes.func,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hidden: PropTypes.bool,
|
||||
hideButtons: PropTypes.bool,
|
||||
|
|
|
@ -35,7 +35,7 @@ export default class AutosuggestEmoji extends PureComponent {
|
|||
alt={emoji.native || emoji.colons}
|
||||
/>
|
||||
|
||||
{emoji.colons}
|
||||
<div className='autosuggest-emoji__name'>{emoji.colons}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
|
||||
interface Props {
|
||||
|
@ -16,27 +14,18 @@ interface Props {
|
|||
};
|
||||
}
|
||||
|
||||
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => {
|
||||
const weeklyUses = tag.history && (
|
||||
<ShortNumber
|
||||
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='autosuggest-hashtag'>
|
||||
<div className='autosuggest-hashtag__name'>
|
||||
#<strong>{tag.name}</strong>
|
||||
</div>
|
||||
{tag.history !== undefined && (
|
||||
<div className='autosuggest-hashtag__uses'>
|
||||
<FormattedMessage
|
||||
id='autosuggest_hashtag.per_week'
|
||||
defaultMessage='{count} per week'
|
||||
values={{ count: weeklyUses }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => (
|
||||
<div className='autosuggest-hashtag'>
|
||||
<div className='autosuggest-hashtag__name'>
|
||||
#<strong>{tag.name}</strong>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
{tag.history !== undefined && (
|
||||
<div className='autosuggest-hashtag__uses'>
|
||||
<ShortNumber
|
||||
value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,6 +5,8 @@ import classNames from 'classnames';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||
|
||||
import AutosuggestEmoji from './autosuggest_emoji';
|
||||
|
@ -195,34 +197,37 @@ export default class AutosuggestInput extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='autosuggest-input'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||
<input
|
||||
type='text'
|
||||
ref={this.setInput}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
dir='auto'
|
||||
aria-autocomplete='list'
|
||||
aria-label={placeholder}
|
||||
id={id}
|
||||
className={className}
|
||||
maxLength={maxLength}
|
||||
lang={lang}
|
||||
spellCheck={spellCheck}
|
||||
/>
|
||||
|
||||
<input
|
||||
type='text'
|
||||
ref={this.setInput}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
dir='auto'
|
||||
aria-autocomplete='list'
|
||||
id={id}
|
||||
className={className}
|
||||
maxLength={maxLength}
|
||||
lang={lang}
|
||||
spellCheck={spellCheck}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
||||
{suggestions.map(this.renderSuggestion)}
|
||||
</div>
|
||||
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={this.input} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props }) => (
|
||||
<div {...props}>
|
||||
<div className='autosuggest-textarea__suggestions' style={{ width: this.input?.clientWidth }}>
|
||||
{suggestions.map(this.renderSuggestion)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import classNames from 'classnames';
|
|||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
import Textarea from 'react-textarea-autosize';
|
||||
|
||||
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
|
||||
|
@ -52,7 +53,6 @@ const AutosuggestTextarea = forwardRef(({
|
|||
onFocus,
|
||||
autoFocus = true,
|
||||
lang,
|
||||
children,
|
||||
}, textareaRef) => {
|
||||
|
||||
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
||||
|
@ -183,40 +183,38 @@ const AutosuggestTextarea = forwardRef(({
|
|||
);
|
||||
};
|
||||
|
||||
return [
|
||||
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
||||
<div className='autosuggest-textarea'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||
return (
|
||||
<div className='autosuggest-textarea'>
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
className='autosuggest-textarea__textarea'
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onPaste={handlePaste}
|
||||
dir='auto'
|
||||
aria-autocomplete='list'
|
||||
aria-label={placeholder}
|
||||
lang={lang}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
className='autosuggest-textarea__textarea'
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onPaste={handlePaste}
|
||||
dir='auto'
|
||||
aria-autocomplete='list'
|
||||
lang={lang}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{children}
|
||||
</div>,
|
||||
|
||||
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
|
||||
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
||||
{suggestions.map(renderSuggestion)}
|
||||
</div>
|
||||
</div>,
|
||||
];
|
||||
<Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={textareaRef} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props }) => (
|
||||
<div {...props}>
|
||||
<div className='autosuggest-textarea__suggestions' style={{ width: textareaRef.current?.clientWidth }}>
|
||||
{suggestions.map(renderSuggestion)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
AutosuggestTextarea.propTypes = {
|
||||
|
@ -232,7 +230,6 @@ AutosuggestTextarea.propTypes = {
|
|||
onKeyDown: PropTypes.func,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
onFocus:PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
autoFocus: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
};
|
||||
|
|
|
@ -165,7 +165,7 @@ class Dropdown extends PureComponent {
|
|||
children: PropTypes.node,
|
||||
icon: PropTypes.string,
|
||||
iconComponent: PropTypes.func,
|
||||
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired,
|
||||
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]),
|
||||
loading: PropTypes.bool,
|
||||
size: PropTypes.number,
|
||||
title: PropTypes.string,
|
||||
|
|
|
@ -76,6 +76,10 @@ export const defaultMediaVisibility = (status) => {
|
|||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' },
|
||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers' },
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Specific people' },
|
||||
edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
|
||||
});
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ import PublicUnlistedIcon from '@/material-icons/400-24px/cloud.svg?react';
|
|||
import MutualIcon from '@/material-icons/400-24px/compare_arrows.svg?react';
|
||||
import LoginIcon from '@/material-icons/400-24px/key.svg?react';
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
import LockOpenIcon from '@/material-icons/400-24px/no_encryption.svg?react';
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
|
||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||
import LimitedIcon from '@/material-icons/400-24px/shield.svg?react';
|
||||
import PersonalIcon from '@/material-icons/400-24px/sticky_note.svg?react';
|
||||
|
@ -33,11 +33,17 @@ const messages = defineMessages({
|
|||
id: 'privacy.public_unlisted.short',
|
||||
defaultMessage: 'Public unlisted',
|
||||
},
|
||||
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' },
|
||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
login_short: {
|
||||
id: 'privacy.login.short',
|
||||
defaultMessage: 'Login only',
|
||||
},
|
||||
unlisted_short: {
|
||||
id: 'privacy.unlisted.short',
|
||||
defaultMessage: 'Quiet public',
|
||||
},
|
||||
private_short: {
|
||||
id: 'privacy.private.short',
|
||||
defaultMessage: 'Followers only',
|
||||
defaultMessage: 'Followers',
|
||||
},
|
||||
limited_short: {
|
||||
id: 'privacy.limited.short',
|
||||
|
@ -61,7 +67,7 @@ const messages = defineMessages({
|
|||
},
|
||||
direct_short: {
|
||||
id: 'privacy.direct.short',
|
||||
defaultMessage: 'Mentioned people only',
|
||||
defaultMessage: 'Specific people',
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -88,7 +94,7 @@ export const VisibilityIcon: React.FC<{ visibility: Visibility }> = ({
|
|||
},
|
||||
unlisted: {
|
||||
icon: 'unlock',
|
||||
iconComponent: LockOpenIcon,
|
||||
iconComponent: QuietTimeIcon,
|
||||
text: intl.formatMessage(messages.unlisted_short),
|
||||
},
|
||||
private: {
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import { PureComponent } from 'react';
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { fetchCustomEmojis } from '../actions/custom_emojis';
|
||||
import { hydrateStore } from '../actions/store';
|
||||
import Compose from '../features/standalone/compose';
|
||||
import initialState from '../initial_state';
|
||||
import { IntlProvider } from '../locales';
|
||||
import { store } from '../store';
|
||||
|
||||
import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
|
||||
import { hydrateStore } from 'mastodon/actions/store';
|
||||
import { Router } from 'mastodon/components/router';
|
||||
import Compose from 'mastodon/features/standalone/compose';
|
||||
import initialState from 'mastodon/initial_state';
|
||||
import { IntlProvider } from 'mastodon/locales';
|
||||
import { store } from 'mastodon/store';
|
||||
|
||||
if (initialState) {
|
||||
store.dispatch(hydrateStore(initialState));
|
||||
|
@ -16,16 +14,14 @@ if (initialState) {
|
|||
|
||||
store.dispatch(fetchCustomEmojis());
|
||||
|
||||
export default class ComposeContainer extends PureComponent {
|
||||
const ComposeContainer = () => (
|
||||
<IntlProvider>
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<Compose />
|
||||
</Router>
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
render () {
|
||||
return (
|
||||
<IntlProvider>
|
||||
<Provider store={store}>
|
||||
<Compose />
|
||||
</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export default ComposeContainer;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import MenuIcon from '@/material-icons/400-24px/menu.svg?react';
|
||||
|
||||
import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||
import { logOut } from 'mastodon/utils/log_out';
|
||||
|
||||
const messages = defineMessages({
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
|
@ -25,53 +25,54 @@ const messages = defineMessages({
|
|||
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
|
||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
||||
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
||||
});
|
||||
|
||||
class ActionBar extends PureComponent {
|
||||
export const ActionBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
onLogout: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
const handleLogoutClick = useCallback(() => {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.logoutMessage),
|
||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||
closeWhenConfirm: false,
|
||||
onConfirm: () => logOut(),
|
||||
},
|
||||
}));
|
||||
}, [dispatch, intl]);
|
||||
|
||||
handleLogout = () => {
|
||||
this.props.onLogout();
|
||||
};
|
||||
let menu = [];
|
||||
|
||||
render () {
|
||||
const { intl } = this.props;
|
||||
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
|
||||
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
|
||||
menu.push({ text: intl.formatMessage(messages.reaction_deck), to: '/reaction_deck' });
|
||||
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
||||
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
|
||||
menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' });
|
||||
menu.push({ text: intl.formatMessage(messages.emoji_reactions), to: '/emoji_reactions' });
|
||||
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
||||
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.logout), action: handleLogoutClick });
|
||||
|
||||
let menu = [];
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' });
|
||||
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' });
|
||||
menu.push({ text: intl.formatMessage(messages.reaction_deck), to: '/reaction_deck' });
|
||||
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
||||
menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmark_categories' });
|
||||
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
|
||||
menu.push({ text: intl.formatMessage(messages.emoji_reactions), to: '/emoji_reactions' });
|
||||
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
||||
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout });
|
||||
|
||||
return (
|
||||
<div className='compose__action-bar'>
|
||||
<div className='compose__action-bar-dropdown'>
|
||||
<DropdownMenuContainer items={menu} icon='bars' iconComponent={MenuIcon} size={24} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(ActionBar);
|
||||
return (
|
||||
<DropdownMenuContainer
|
||||
items={menu}
|
||||
icon='bars'
|
||||
iconComponent={MoreHorizIcon}
|
||||
size={24}
|
||||
direction='right'
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -15,7 +15,7 @@ export default class AutosuggestAccount extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<div className='autosuggest-account' title={account.get('acct')}>
|
||||
<div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div>
|
||||
<Avatar account={account} size={24} />
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,26 +1,18 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { length } from 'stringz';
|
||||
|
||||
export default class CharacterCounter extends PureComponent {
|
||||
export const CharacterCounter = ({ text, max }) => {
|
||||
const diff = max - length(text);
|
||||
|
||||
static propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
checkRemainingText (diff) {
|
||||
if (diff < 0) {
|
||||
return <span className='character-counter character-counter--over'>{diff}</span>;
|
||||
}
|
||||
|
||||
return <span className='character-counter'>{diff}</span>;
|
||||
if (diff < 0) {
|
||||
return <span className='character-counter character-counter--over'>{diff}</span>;
|
||||
}
|
||||
|
||||
render () {
|
||||
const diff = this.props.max - length(this.props.text);
|
||||
return this.checkRemainingText(diff);
|
||||
}
|
||||
return <span className='character-counter'>{diff}</span>;
|
||||
};
|
||||
|
||||
}
|
||||
CharacterCounter.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,262 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import CircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
add_expiration: { id: 'status.expiration.add', defaultMessage: 'Set status expiration' },
|
||||
});
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
||||
|
||||
class CircleDropdownMenu extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
style: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
value: PropTypes.string,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleDocumentClick = e => {
|
||||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = e => {
|
||||
const { items } = this.props;
|
||||
const value = e.currentTarget.getAttribute('data-index');
|
||||
const index = items.findIndex(item => {
|
||||
return (item.value === value);
|
||||
});
|
||||
let element = null;
|
||||
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
this.props.onClose();
|
||||
break;
|
||||
case 'Enter':
|
||||
this.handleClick(e);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
element = this.node.childNodes[index + 1] || this.node.firstChild;
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
element = this.node.childNodes[index - 1] || this.node.lastChild;
|
||||
break;
|
||||
case 'Tab':
|
||||
if (e.shiftKey) {
|
||||
element = this.node.childNodes[index - 1] || this.node.lastChild;
|
||||
} else {
|
||||
element = this.node.childNodes[index + 1] || this.node.firstChild;
|
||||
}
|
||||
break;
|
||||
case 'Home':
|
||||
element = this.node.firstChild;
|
||||
break;
|
||||
case 'End':
|
||||
element = this.node.lastChild;
|
||||
break;
|
||||
}
|
||||
|
||||
if (element) {
|
||||
element.focus();
|
||||
this.props.onChange(element.getAttribute('data-index'));
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = e => {
|
||||
const value = e.currentTarget.getAttribute('data-index');
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onClose();
|
||||
this.props.onChange(value);
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
document.addEventListener('click', this.handleDocumentClick, { capture: true });
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
if (this.focusedItem) this.focusedItem.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('click', this.handleDocumentClick, { capture: true });
|
||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
};
|
||||
|
||||
setFocusRef = c => {
|
||||
this.focusedItem = c;
|
||||
};
|
||||
|
||||
render () {
|
||||
const { style, items, value } = this.props;
|
||||
|
||||
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 className='privacy-dropdown__option__content'>
|
||||
<strong>{item.text}</strong>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class CircleDropdown extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isUserTouching: PropTypes.func,
|
||||
onModalOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
noDirect: PropTypes.bool,
|
||||
container: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
circles: ImmutablePropTypes.map,
|
||||
circleId: PropTypes.string,
|
||||
unavailable: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
open: false,
|
||||
placement: 'bottom',
|
||||
};
|
||||
|
||||
handleToggle = () => {
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
this.setState({ open: !this.state.open });
|
||||
};
|
||||
|
||||
handleModalActionClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { value } = this.options[e.currentTarget.getAttribute('data-index')];
|
||||
|
||||
this.props.onModalClose();
|
||||
this.props.onChange(value);
|
||||
};
|
||||
|
||||
handleKeyDown = e => {
|
||||
switch(e.key) {
|
||||
case 'Escape':
|
||||
this.handleClose();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseDown = () => {
|
||||
if (!this.state.open) {
|
||||
this.activeElement = document.activeElement;
|
||||
}
|
||||
};
|
||||
|
||||
handleButtonKeyDown = (e) => {
|
||||
switch(e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
this.handleMouseDown();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ open: false });
|
||||
};
|
||||
|
||||
handleChange = value => {
|
||||
this.props.onChange(value);
|
||||
};
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
};
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
};
|
||||
|
||||
handleOverlayEnter = (state) => {
|
||||
this.setState({ placement: state.placement });
|
||||
};
|
||||
|
||||
render () {
|
||||
const { container, disabled, intl, unavailable, circles, circleId } = this.props;
|
||||
const { open, placement } = this.state;
|
||||
|
||||
if (unavailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const listOptions = circles.toArray().filter((circle) => circle && circle.length > 1).map((circle) => {
|
||||
return circle[1] ? { value: circle[1].get('id'), text: circle[1].get('title') } : null;
|
||||
}).filter((opt) => opt !== null);
|
||||
const listOption = listOptions.find((opt) => opt.value === circleId) ?? { value: '0', text: 'Unselected' };
|
||||
|
||||
return (
|
||||
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
|
||||
<button
|
||||
type='button'
|
||||
title={intl.formatMessage(messages.add_expiration)}
|
||||
aria-expanded={open}
|
||||
onClick={this.handleToggle}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
disabled={disabled}
|
||||
className={classNames('dropdown-button', { active: open })}
|
||||
>
|
||||
<Icon id='clock-o' icon={CircleIcon} />
|
||||
<span className='dropdown-button__label'>{listOption.text}</span>
|
||||
</button>
|
||||
|
||||
<Overlay show={open} offset={[5, 5]} placement={placement} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
||||
<CircleDropdownMenu
|
||||
items={listOptions}
|
||||
value={circleId}
|
||||
onClose={this.handleClose}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(CircleDropdown);
|
|
@ -1,59 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
|
||||
|
||||
import Select, { NonceProvider } from 'react-select';
|
||||
|
||||
class CircleSelect extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
unavailable: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
circles: ImmutablePropTypes.map,
|
||||
circleId: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleClick = value => {
|
||||
this.props.onChange(value.value);
|
||||
};
|
||||
|
||||
noOptionsMessage = () => '';
|
||||
|
||||
render () {
|
||||
const { unavailable, circles, circleId } = this.props;
|
||||
|
||||
if (unavailable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const listOptions = circles.toArray().filter((circle) => circle).map((circle) => {
|
||||
return { value: circle[1].get('id'), label: circle[1].get('title') };
|
||||
});
|
||||
const listValue = listOptions.find((opt) => opt.value === circleId);
|
||||
|
||||
return (
|
||||
<div className='compose-form__circle-select'>
|
||||
<NonceProvider nonce={document.querySelector('meta[name=style-nonce]').content} cacheKey='circles'>
|
||||
<Select
|
||||
value={listValue}
|
||||
options={listOptions}
|
||||
noOptionsMessage={this.noOptionsMessage}
|
||||
onChange={this.handleClick}
|
||||
className='column-content-select__container'
|
||||
classNamePrefix='column-content-select'
|
||||
name='circles'
|
||||
defaultOptions
|
||||
/>
|
||||
</NonceProvider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(CircleSelect);
|
|
@ -10,23 +10,19 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
|
||||
import { length } from 'stringz';
|
||||
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
import LimitedIcon from '@/material-icons/400-24px/shield.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
|
||||
|
||||
|
||||
import AutosuggestInput from '../../../components/autosuggest_input';
|
||||
import AutosuggestTextarea from '../../../components/autosuggest_textarea';
|
||||
import { Button } from '../../../components/button';
|
||||
import CircleSelectContainer from '../containers/circle_select_container';
|
||||
import CircleDropdownContainer from '../containers/circle_dropdown_container';
|
||||
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
|
||||
import ExpirationDropdownContainer from '../containers/expiration_dropdown_container';
|
||||
import LanguageDropdown from '../containers/language_dropdown_container';
|
||||
import MarkdownButtonContainer from '../containers/markdown_button_container';
|
||||
import PollButtonContainer from '../containers/poll_button_container';
|
||||
import PollFormContainer from '../containers/poll_form_container';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
|
||||
import SearchabilityDropdownContainer from '../containers/searchability_dropdown_container';
|
||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||
import UploadButtonContainer from '../containers/upload_button_container';
|
||||
|
@ -34,16 +30,20 @@ import UploadFormContainer from '../containers/upload_form_container';
|
|||
import WarningContainer from '../containers/warning_container';
|
||||
import { countableText } from '../util/counter';
|
||||
|
||||
import CharacterCounter from './character_counter';
|
||||
import { CharacterCounter } from './character_counter';
|
||||
import { EditIndicator } from './edit_indicator';
|
||||
import { NavigationBar } from './navigation_bar';
|
||||
import { PollForm } from "./poll_form";
|
||||
import { ReplyIndicator } from './reply_indicator';
|
||||
|
||||
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
|
||||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
|
||||
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' },
|
||||
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' },
|
||||
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning (optional)' },
|
||||
publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
|
||||
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Update' },
|
||||
reply: { id: 'compose_form.reply', defaultMessage: 'Reply' },
|
||||
});
|
||||
|
||||
class ComposeForm extends ImmutablePureComponent {
|
||||
|
@ -72,6 +72,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
onPickEmoji: PropTypes.func.isRequired,
|
||||
onPickExpiration: PropTypes.func.isRequired,
|
||||
autoFocus: PropTypes.bool,
|
||||
withoutNavigation: PropTypes.bool,
|
||||
anyMedia: PropTypes.bool,
|
||||
isInReply: PropTypes.bool,
|
||||
singleColumn: PropTypes.bool,
|
||||
|
@ -237,102 +238,100 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { intl, onPaste, autoFocus } = this.props;
|
||||
const { intl, onPaste, autoFocus, withoutNavigation } = this.props;
|
||||
const { highlighted } = this.state;
|
||||
const disabled = this.props.isSubmitting;
|
||||
|
||||
let publishText = '';
|
||||
|
||||
if (this.props.isEditing) {
|
||||
publishText = intl.formatMessage(messages.saveChanges);
|
||||
} else if (this.props.privacy === 'private' || this.props.privacy === 'direct') {
|
||||
publishText = <><Icon id='lock' icon={LockIcon} /> {intl.formatMessage(messages.publish)}</>;
|
||||
} else if (['circle', 'mutual', 'limited', 'reply'].includes(this.props.privacy)) {
|
||||
publishText = <><Icon id='get-pocket' icon={LimitedIcon} /> {intl.formatMessage(messages.publish)}</>;
|
||||
} else {
|
||||
publishText = this.props.privacy === 'public' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||
}
|
||||
|
||||
return (
|
||||
<form className='compose-form' onSubmit={this.handleSubmit}>
|
||||
<ReplyIndicator />
|
||||
{!withoutNavigation && <NavigationBar />}
|
||||
<WarningContainer />
|
||||
|
||||
<ReplyIndicatorContainer />
|
||||
<div className={classNames('compose-form__highlightable', { active: highlighted })} ref={this.setRef}>
|
||||
<div className='compose-form__scrollable'>
|
||||
<EditIndicator />
|
||||
|
||||
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
||||
value={this.props.spoilerText}
|
||||
onChange={this.handleChangeSpoilerText}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
disabled={!this.props.spoiler}
|
||||
ref={this.setSpoilerText}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSpoilerSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
id='cw-spoiler-input'
|
||||
className='spoiler-input__input'
|
||||
lang={this.props.lang}
|
||||
spellCheck
|
||||
/>
|
||||
</div>
|
||||
{this.props.spoiler && (
|
||||
<div className='spoiler-input'>
|
||||
<div className='spoiler-input__border' />
|
||||
|
||||
<div className={classNames('compose-form__highlightable', { active: highlighted })}>
|
||||
<AutosuggestTextarea
|
||||
ref={this.textareaRef}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
disabled={disabled}
|
||||
value={this.props.text}
|
||||
onChange={this.handleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
onFocus={this.handleFocus}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
onPaste={onPaste}
|
||||
autoFocus={autoFocus}
|
||||
lang={this.props.lang}
|
||||
>
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
|
||||
value={this.props.spoilerText}
|
||||
disabled={disabled}
|
||||
onChange={this.handleChangeSpoilerText}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
ref={this.setSpoilerText}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSpoilerSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
id='cw-spoiler-input'
|
||||
className='spoiler-input__input'
|
||||
lang={this.props.lang}
|
||||
spellCheck
|
||||
/>
|
||||
|
||||
<div className='compose-form__modifiers'>
|
||||
<UploadFormContainer />
|
||||
<PollFormContainer />
|
||||
</div>
|
||||
</AutosuggestTextarea>
|
||||
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
|
||||
<ExpirationDropdownContainer onPickExpiration={this.handleExpirationPick} />
|
||||
<div className='spoiler-input__border' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className='compose-form__buttons-wrapper'>
|
||||
<div className='compose-form__buttons'>
|
||||
<UploadButtonContainer />
|
||||
<PollButtonContainer />
|
||||
<PrivacyDropdownContainer disabled={this.props.isEditing} />
|
||||
<SearchabilityDropdownContainer disabled={this.props.isEditing} />
|
||||
<SpoilerButtonContainer />
|
||||
<LanguageDropdown />
|
||||
<MarkdownButtonContainer />
|
||||
</div>
|
||||
|
||||
<div className='character-counter__wrapper'>
|
||||
<CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CircleSelectContainer />
|
||||
|
||||
<div className='compose-form__publish'>
|
||||
<div className='compose-form__publish-button-wrapper'>
|
||||
<Button
|
||||
type='submit'
|
||||
text={publishText}
|
||||
disabled={!this.canSubmit()}
|
||||
block
|
||||
<AutosuggestTextarea
|
||||
ref={this.textareaRef}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
disabled={disabled}
|
||||
value={this.props.text}
|
||||
onChange={this.handleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
onFocus={this.handleFocus}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
onPaste={onPaste}
|
||||
autoFocus={autoFocus}
|
||||
lang={this.props.lang}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UploadFormContainer />
|
||||
<PollForm />
|
||||
|
||||
<div className='compose-form__footer'>
|
||||
<div className='compose-form__dropdowns'>
|
||||
<PrivacyDropdownContainer disabled={this.props.isEditing} />
|
||||
<LanguageDropdown />
|
||||
</div>
|
||||
<div className='compose-form__dropdowns compose-form__dropdowns__second'>
|
||||
<CircleDropdownContainer />
|
||||
</div>
|
||||
<div className='compose-form__dropdowns compose-form__dropdowns__second'>
|
||||
<SearchabilityDropdownContainer disabled={this.props.isEditing} />
|
||||
<ExpirationDropdownContainer onPickExpiration={this.handleExpirationPick} />
|
||||
</div>
|
||||
|
||||
<div className='compose-form__actions'>
|
||||
<div className='compose-form__buttons'>
|
||||
<UploadButtonContainer />
|
||||
<PollButtonContainer />
|
||||
<SpoilerButtonContainer />
|
||||
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
|
||||
<MarkdownButtonContainer />
|
||||
<CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} />
|
||||
</div>
|
||||
|
||||
|
||||
<div className='compose-form__submit'>
|
||||
<Button
|
||||
type='submit'
|
||||
text={intl.formatMessage(this.props.isEditing ? messages.saveChanges : (this.props.isInReply ? messages.reply : messages.publish))}
|
||||
disabled={!this.canSubmit()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import BarChart4BarsIcon from 'mastodon/../material-icons/400-24px/bar_chart_4_bars.svg?react';
|
||||
import CloseIcon from 'mastodon/../material-icons/400-24px/close.svg?react';
|
||||
import PhotoLibraryIcon from 'mastodon/../material-icons/400-24px/photo_library.svg?react';
|
||||
import { cancelReplyCompose } from 'mastodon/actions/compose';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||
});
|
||||
|
||||
export const EditIndicator = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const id = useSelector(state => state.getIn(['compose', 'id']));
|
||||
const status = useSelector(state => state.getIn(['statuses', id]));
|
||||
const account = useSelector(state => state.getIn(['accounts', status?.get('account')]));
|
||||
|
||||
const handleCancelClick = useCallback(() => {
|
||||
dispatch(cancelReplyCompose());
|
||||
}, [dispatch]);
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
|
||||
return (
|
||||
<div className='edit-indicator'>
|
||||
<div className='edit-indicator__header'>
|
||||
<div className='edit-indicator__display-name'>
|
||||
<Link to={`/@${account.get('acct')}`}>@{account.get('acct')}</Link>
|
||||
·
|
||||
<Link to={`/@${account.get('acct')}/${status.get('id')}`}><RelativeTimestamp timestamp={status.get('created_at')} /></Link>
|
||||
</div>
|
||||
|
||||
<div className='edit-indicator__cancel'>
|
||||
<IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={handleCancelClick} inverted />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='edit-indicator__content translate' dangerouslySetInnerHTML={content} />
|
||||
|
||||
{(status.get('poll') || status.get('media_attachments').size > 0) && (
|
||||
<div className='edit-indicator__attachments'>
|
||||
{status.get('poll') && <><Icon icon={BarChart4BarsIcon} /><FormattedMessage id='reply_indicator.poll' defaultMessage='Poll' /></>}
|
||||
{status.get('media_attachments').size > 0 && <><Icon icon={PhotoLibraryIcon} /><FormattedMessage id='reply_indicator.attachments' defaultMessage='{count, plural, one {# attachment} other {# attachments}}' values={{ count: status.get('media_attachments').size }} /></>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -10,6 +10,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import MoodIcon from 'mastodon/../material-icons/400-24px/mood.svg?react';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { assetHost } from 'mastodon/utils/config';
|
||||
|
||||
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
|
||||
|
@ -321,7 +323,6 @@ class EmojiPickerDropdown extends PureComponent {
|
|||
onPickEmoji: PropTypes.func.isRequired,
|
||||
onSkinTone: PropTypes.func.isRequired,
|
||||
skinTone: PropTypes.number.isRequired,
|
||||
button: PropTypes.node,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -398,23 +399,24 @@ class EmojiPickerDropdown extends PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props;
|
||||
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props;
|
||||
const title = intl.formatMessage(messages.emoji);
|
||||
const { active, loading, bottom } = this.state;
|
||||
|
||||
return (
|
||||
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
|
||||
<div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
|
||||
{button || <img
|
||||
className={classNames('emojione', { 'pulse-loading': active && loading })}
|
||||
alt='🙂'
|
||||
src={`${assetHost}/emoji/1f642.svg`}
|
||||
/>}
|
||||
</div>
|
||||
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown} ref={this.setTargetRef}>
|
||||
<IconButton
|
||||
title={title}
|
||||
aria-expanded={active}
|
||||
active={active}
|
||||
iconComponent={MoodIcon}
|
||||
onClick={this.onToggle}
|
||||
inverted
|
||||
/>
|
||||
|
||||
<Overlay show={active} placement={ bottom ? 'bottom' : 'top' } target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
|
||||
{({ props, placement })=> (
|
||||
<div {...props} style={{ ...props.style, width: 299 }}>
|
||||
<div {...props} style={{ ...props.style }}>
|
||||
<div className={`dropdown-animation ${placement}`}>
|
||||
<EmojiPickerMenu
|
||||
custom_emojis={this.props.custom_emojis}
|
||||
|
|
|
@ -9,8 +9,7 @@ import { supportsPassiveEvents } from 'detect-passive-events';
|
|||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import TimerIcon from '@/material-icons/400-24px/timer.svg?react';
|
||||
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
add_expiration: { id: 'status.expiration.add', defaultMessage: 'Set status expiration' },
|
||||
|
@ -23,7 +22,6 @@ class ExpirationDropdownMenu extends PureComponent {
|
|||
static propTypes = {
|
||||
style: PropTypes.object,
|
||||
items: PropTypes.array.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -108,13 +106,13 @@ class ExpirationDropdownMenu extends PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { style, items, value } = this.props;
|
||||
const { style, items } = this.props;
|
||||
|
||||
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 className='expiration-dropdown__option__content'>
|
||||
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option')} aria-selected={false} ref={null}>
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{item.text}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -131,7 +129,6 @@ class ExpirationDropdown extends PureComponent {
|
|||
isUserTouching: PropTypes.func,
|
||||
onModalOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
noDirect: PropTypes.bool,
|
||||
container: PropTypes.func,
|
||||
|
@ -145,21 +142,11 @@ class ExpirationDropdown extends PureComponent {
|
|||
};
|
||||
|
||||
handleToggle = () => {
|
||||
if (this.props.isUserTouching && this.props.isUserTouching()) {
|
||||
if (this.state.open) {
|
||||
this.props.onModalClose();
|
||||
} else {
|
||||
this.props.onModalOpen({
|
||||
actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
|
||||
onClick: this.handleModalActionClick,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ open: !this.state.open });
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
this.setState({ open: !this.state.open });
|
||||
};
|
||||
|
||||
handleModalActionClick = (e) => {
|
||||
|
@ -230,36 +217,30 @@ class ExpirationDropdown extends PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { value, container, disabled, intl } = this.props;
|
||||
const { container, disabled, intl } = this.props;
|
||||
const { open, placement } = this.state;
|
||||
|
||||
return (
|
||||
<div className={classNames('expiration-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}>
|
||||
<div className={classNames('expiration-dropdown__value')} ref={this.setTargetRef}>
|
||||
<IconButton
|
||||
className='expiration-dropdown__value-icon'
|
||||
icon='clock-o'
|
||||
iconComponent={TimerIcon}
|
||||
title={intl.formatMessage(messages.add_expiration)}
|
||||
size={18}
|
||||
expanded={open}
|
||||
active={open}
|
||||
inverted
|
||||
onClick={this.handleToggle}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
style={{ height: null, lineHeight: '27px' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
|
||||
<button
|
||||
type='button'
|
||||
title={intl.formatMessage(messages.add_expiration)}
|
||||
aria-expanded={open}
|
||||
onClick={this.handleToggle}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
disabled={disabled}
|
||||
className={classNames('dropdown-button', { active: open })}
|
||||
>
|
||||
<Icon id='clock-o' icon={TimerIcon} />
|
||||
</button>
|
||||
|
||||
<Overlay show={open} placement={'bottom'} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
<Overlay show={open} offset={[5, 5]} placement={placement} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation expiration-dropdown__dropdown ${placement}`}>
|
||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
||||
<ExpirationDropdownMenu
|
||||
items={this.options}
|
||||
value={value}
|
||||
onClose={this.handleClose}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
|
|
|
@ -9,10 +9,11 @@ import { supportsPassiveEvents } from 'detect-passive-events';
|
|||
import fuzzysort from 'fuzzysort';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import CancelIcon from 'mastodon/../material-icons/400-24px/cancel-fill.svg?react';
|
||||
import SearchIcon from 'mastodon/../material-icons/400-24px/search.svg?react';
|
||||
import TranslateIcon from 'mastodon/../material-icons/400-24px/translate.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { languages as preloadedLanguages } from 'mastodon/initial_state';
|
||||
import { loupeIcon, deleteIcon } from 'mastodon/utils/icons';
|
||||
|
||||
import TextIconButton from './text_icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' },
|
||||
|
@ -231,7 +232,7 @@ class LanguageDropdownMenu extends PureComponent {
|
|||
<div ref={this.setRef}>
|
||||
<div className='emoji-mart-search'>
|
||||
<input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} />
|
||||
<button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button>
|
||||
<button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}><Icon icon={!isSearching ? SearchIcon : CancelIcon} /></button>
|
||||
</div>
|
||||
|
||||
<div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
|
||||
|
@ -297,20 +298,24 @@ class LanguageDropdown extends PureComponent {
|
|||
render () {
|
||||
const { value, intl, frequentlyUsedLanguages } = this.props;
|
||||
const { open, placement } = this.state;
|
||||
const current = preloadedLanguages.find(lang => lang[0] === value) ?? [];
|
||||
|
||||
return (
|
||||
<div className={classNames('privacy-dropdown', placement, { active: open })}>
|
||||
<div className='privacy-dropdown__value' ref={this.setTargetRef} >
|
||||
<TextIconButton
|
||||
className='privacy-dropdown__value-icon'
|
||||
label={value && value.toUpperCase()}
|
||||
title={intl.formatMessage(messages.changeLanguage)}
|
||||
active={open}
|
||||
onClick={this.handleToggle}
|
||||
/>
|
||||
</div>
|
||||
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
|
||||
<button
|
||||
type='button'
|
||||
title={intl.formatMessage(messages.changeLanguage)}
|
||||
aria-expanded={open}
|
||||
onClick={this.handleToggle}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
className={classNames('dropdown-button', { active: open })}
|
||||
>
|
||||
<Icon icon={TranslateIcon} />
|
||||
<span className='dropdown-button__label'>{current[2] ?? value}</span>
|
||||
</button>
|
||||
|
||||
<Overlay show={open} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
<Overlay show={open} offset={[5, 5]} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation language-dropdown__dropdown ${placement}`} >
|
||||
|
|
|
@ -1,50 +1,36 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import CloseIcon from 'mastodon/../material-icons/400-24px/close.svg?react';
|
||||
import { cancelReplyCompose } from 'mastodon/actions/compose';
|
||||
import Account from 'mastodon/components/account';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { ActionBar } from './action_bar';
|
||||
|
||||
import ActionBar from './action_bar';
|
||||
|
||||
export default class NavigationBar extends ImmutablePureComponent {
|
||||
const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||
});
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
onLogout: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
export const NavigationBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const account = useSelector(state => state.getIn(['accounts', me]));
|
||||
const isReplying = useSelector(state => !!state.getIn(['compose', 'in_reply_to']));
|
||||
|
||||
render () {
|
||||
const username = this.props.account.get('acct');
|
||||
return (
|
||||
<div className='navigation-bar'>
|
||||
<Link to={`/@${username}`}>
|
||||
<span style={{ display: 'none' }}>{username}</span>
|
||||
<Avatar account={this.props.account} size={46} />
|
||||
</Link>
|
||||
const handleCancelClick = useCallback(() => {
|
||||
dispatch(cancelReplyCompose());
|
||||
}, [dispatch]);
|
||||
|
||||
<div className='navigation-bar__profile'>
|
||||
<span>
|
||||
<Link to={`/@${username}`}>
|
||||
<strong className='navigation-bar__profile-account'>@{username}</strong>
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='navigation-bar__actions'>
|
||||
<ActionBar account={this.props.account} onLogout={this.props.onLogout} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
return (
|
||||
<div className='navigation-bar'>
|
||||
<Account account={account} minimal />
|
||||
{isReplying ? <IconButton title={intl.formatMessage(messages.cancel)} iconComponent={CloseIcon} onClick={handleCancelClick} /> : <ActionBar />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,11 +3,10 @@ import { PureComponent } from 'react';
|
|||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
|
||||
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
|
||||
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' },
|
||||
remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' },
|
||||
|
@ -22,7 +21,6 @@ class PollButton extends PureComponent {
|
|||
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
unavailable: PropTypes.bool,
|
||||
active: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
|
@ -33,17 +31,13 @@ class PollButton extends PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { intl, active, unavailable, disabled } = this.props;
|
||||
|
||||
if (unavailable) {
|
||||
return null;
|
||||
}
|
||||
const { intl, active, disabled } = this.props;
|
||||
|
||||
return (
|
||||
<div className='compose-form__poll-button'>
|
||||
<IconButton
|
||||
icon='tasks'
|
||||
iconComponent={InsertChartIcon}
|
||||
iconComponent={BarChart4BarsIcon}
|
||||
title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)}
|
||||
disabled={disabled}
|
||||
onClick={this.handleClick}
|
||||
|
|
|
@ -1,189 +1,162 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import {
|
||||
changePollSettings,
|
||||
changePollOption,
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
selectComposeSuggestion,
|
||||
} from 'mastodon/actions/compose';
|
||||
import AutosuggestInput from 'mastodon/components/autosuggest_input';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
|
||||
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
|
||||
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
|
||||
poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
|
||||
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Option {number}' },
|
||||
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add option' },
|
||||
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this option' },
|
||||
duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll length' },
|
||||
type: { id: 'compose_form.poll.type', defaultMessage: 'Style' },
|
||||
switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' },
|
||||
switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' },
|
||||
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
||||
singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Pick one' },
|
||||
multipleChoice: { id: 'compose_form.poll.multiple', defaultMessage: 'Multiple choice' },
|
||||
});
|
||||
|
||||
class OptionIntl extends PureComponent {
|
||||
const Select = ({ label, options, value, onChange }) => {
|
||||
return (
|
||||
<label className='compose-form__poll__select'>
|
||||
<span className='compose-form__poll__select__label'>{label}</span>
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
lang: PropTypes.string,
|
||||
index: PropTypes.number.isRequired,
|
||||
isPollMultiple: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onToggleMultiple: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
<select className='compose-form__poll__select__value' value={value} onChange={onChange}>
|
||||
{options.map((option, i) => (
|
||||
<option key={i} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
handleOptionTitleChange = e => {
|
||||
this.props.onChange(this.props.index, e.target.value);
|
||||
};
|
||||
Select.propTypes = {
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
onChange: PropTypes.func,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
})),
|
||||
};
|
||||
|
||||
handleOptionRemove = () => {
|
||||
this.props.onRemove(this.props.index);
|
||||
};
|
||||
const Option = ({ multipleChoice, index, title, autoFocus }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
|
||||
const lang = useSelector(state => state.getIn(['compose', 'language']));
|
||||
|
||||
const handleChange = useCallback(({ target: { value } }) => {
|
||||
dispatch(changePollOption(index, value));
|
||||
}, [dispatch, index]);
|
||||
|
||||
handleToggleMultiple = e => {
|
||||
this.props.onToggleMultiple();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const handleSuggestionsFetchRequested = useCallback(token => {
|
||||
dispatch(fetchComposeSuggestions(token));
|
||||
}, [dispatch]);
|
||||
|
||||
handleCheckboxKeypress = e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
this.handleToggleMultiple(e);
|
||||
}
|
||||
};
|
||||
const handleSuggestionsClearRequested = useCallback(() => {
|
||||
dispatch(clearComposeSuggestions());
|
||||
}, [dispatch]);
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.props.onClearSuggestions();
|
||||
};
|
||||
const handleSuggestionSelected = useCallback((tokenStart, token, value) => {
|
||||
dispatch(selectComposeSuggestion(tokenStart, token, value, ['poll', 'options', index]));
|
||||
}, [dispatch, index]);
|
||||
|
||||
onSuggestionsFetchRequested = (token) => {
|
||||
this.props.onFetchSuggestions(token);
|
||||
};
|
||||
return (
|
||||
<label className={classNames('poll__option editable', { empty: index > 1 && title.length === 0 })}>
|
||||
<span className={classNames('poll__input', { checkbox: multipleChoice })} />
|
||||
|
||||
onSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
|
||||
};
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={50}
|
||||
value={title}
|
||||
lang={lang}
|
||||
spellCheck
|
||||
onChange={handleChange}
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={handleSuggestionsClearRequested}
|
||||
onSuggestionSelected={handleSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { isPollMultiple, title, lang, index, autoFocus, intl } = this.props;
|
||||
Option.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
multipleChoice: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
};
|
||||
|
||||
return (
|
||||
<li>
|
||||
<label className='poll__option editable'>
|
||||
<span
|
||||
className={classNames('poll__input', { checkbox: isPollMultiple })}
|
||||
onClick={this.handleToggleMultiple}
|
||||
onKeyPress={this.handleCheckboxKeypress}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
||||
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
||||
/>
|
||||
export const PollForm = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const poll = useSelector(state => state.getIn(['compose', 'poll']));
|
||||
const options = poll?.get('options');
|
||||
const expiresIn = poll?.get('expires_in');
|
||||
const isMultiple = poll?.get('multiple');
|
||||
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={50}
|
||||
value={title}
|
||||
lang={lang}
|
||||
spellCheck
|
||||
onChange={this.handleOptionTitleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</label>
|
||||
const handleDurationChange = useCallback(({ target: { value } }) => {
|
||||
dispatch(changePollSettings(value, isMultiple));
|
||||
}, [dispatch, isMultiple]);
|
||||
|
||||
<div className='poll__cancel'>
|
||||
<IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' iconComponent={CloseIcon} onClick={this.handleOptionRemove} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
const handleTypeChange = useCallback(({ target: { value } }) => {
|
||||
dispatch(changePollSettings(expiresIn, value === 'true'));
|
||||
}, [dispatch, expiresIn]);
|
||||
|
||||
if (poll === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
return (
|
||||
<div className='compose-form__poll'>
|
||||
{options.map((title, i) => (
|
||||
<Option
|
||||
title={title}
|
||||
key={i}
|
||||
index={i}
|
||||
multipleChoice={isMultiple}
|
||||
autoFocus={i === 0}
|
||||
/>
|
||||
))}
|
||||
|
||||
const Option = injectIntl(OptionIntl);
|
||||
<div className='compose-form__poll__footer'>
|
||||
<Select label={intl.formatMessage(messages.duration)} options={[
|
||||
{ value: 300, label: intl.formatMessage(messages.minutes, { number: 5 })},
|
||||
{ value: 1800, label: intl.formatMessage(messages.minutes, { number: 30 })},
|
||||
{ value: 3600, label: intl.formatMessage(messages.hours, { number: 1 })},
|
||||
{ value: 21600, label: intl.formatMessage(messages.hours, { number: 6 })},
|
||||
{ value: 43200, label: intl.formatMessage(messages.hours, { number: 12 })},
|
||||
{ value: 86400, label: intl.formatMessage(messages.days, { number: 1 })},
|
||||
{ value: 259200, label: intl.formatMessage(messages.days, { number: 3 })},
|
||||
{ value: 604800, label: intl.formatMessage(messages.days, { number: 7 })},
|
||||
]} value={expiresIn} onChange={handleDurationChange} />
|
||||
|
||||
class PollForm extends ImmutablePureComponent {
|
||||
<div className='compose-form__poll__footer__sep' />
|
||||
|
||||
static propTypes = {
|
||||
options: ImmutablePropTypes.list,
|
||||
lang: PropTypes.string,
|
||||
expiresIn: PropTypes.number,
|
||||
isMultiple: PropTypes.bool,
|
||||
onChangeOption: PropTypes.func.isRequired,
|
||||
onAddOption: PropTypes.func.isRequired,
|
||||
onRemoveOption: PropTypes.func.isRequired,
|
||||
onChangeSettings: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleAddOption = () => {
|
||||
this.props.onAddOption('');
|
||||
};
|
||||
|
||||
handleSelectDuration = e => {
|
||||
this.props.onChangeSettings(e.target.value, this.props.isMultiple);
|
||||
};
|
||||
|
||||
handleToggleMultiple = () => {
|
||||
this.props.onChangeSettings(this.props.expiresIn, !this.props.isMultiple);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { options, lang, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;
|
||||
|
||||
if (!options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const autoFocusIndex = options.indexOf('');
|
||||
|
||||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<ul>
|
||||
{options.map((title, i) => <Option title={title} lang={lang} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
|
||||
</ul>
|
||||
|
||||
<div className='poll__footer'>
|
||||
<button type='button' disabled={options.size >= 8} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' icon={AddIcon} /> <FormattedMessage {...messages.add_option} /></button>
|
||||
|
||||
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
|
||||
<select value={expiresIn} onChange={this.handleSelectDuration}>
|
||||
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
|
||||
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
|
||||
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
|
||||
<option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
|
||||
<option value={43200}>{intl.formatMessage(messages.hours, { number: 12 })}</option>
|
||||
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
|
||||
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
|
||||
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
|
||||
</select>
|
||||
</div>
|
||||
<Select label={intl.formatMessage(messages.type)} options={[
|
||||
{ value: false, label: intl.formatMessage(messages.singleChoice) },
|
||||
{ value: true, label: intl.formatMessage(messages.multipleChoice) },
|
||||
]} value={isMultiple} onChange={handleTypeChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(PollForm);
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,7 +5,6 @@ import { injectIntl, defineMessages } from 'react-intl';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
|
@ -13,36 +12,38 @@ import CircleIcon from '@/material-icons/400-24px/account_circle.svg?react';
|
|||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import PublicUnlistedIcon from '@/material-icons/400-24px/cloud.svg?react';
|
||||
import MutualIcon from '@/material-icons/400-24px/compare_arrows.svg?react';
|
||||
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
||||
import LoginIcon from '@/material-icons/400-24px/key.svg?react';
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
import LockOpenIcon from '@/material-icons/400-24px/no_encryption.svg?react';
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
|
||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||
import LimitedIcon from '@/material-icons/400-24px/shield.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { enableLoginPrivacy, enableLocalPrivacy } from 'mastodon/initial_state';
|
||||
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all' },
|
||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but opted-out of discovery features' },
|
||||
public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' },
|
||||
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' },
|
||||
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Fewer algorithmic fanfares' },
|
||||
public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' },
|
||||
public_unlisted_long: { id: 'privacy.public_unlisted.long', defaultMessage: 'Visible for all without GTL' },
|
||||
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' },
|
||||
login_long: { id: 'privacy.login.long', defaultMessage: 'Login user only' },
|
||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
|
||||
private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' },
|
||||
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers' },
|
||||
private_long: { id: 'privacy.private.long', defaultMessage: 'Only your followers' },
|
||||
limited_short: { id: 'privacy.limited.short', defaultMessage: 'Limited' },
|
||||
mutual_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual' },
|
||||
mutual_long: { id: 'privacy.mutual.long', defaultMessage: 'Mutual follows only' },
|
||||
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle' },
|
||||
circle_long: { id: 'privacy.circle.long', defaultMessage: 'Circle members only' },
|
||||
reply_short: { id: 'privacy.reply.short', defaultMessage: 'Reply' },
|
||||
reply_long: { id: 'privacy.reply.long', defaultMessage: 'Reply to limited post' },
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
|
||||
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' },
|
||||
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' },
|
||||
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Specific people' },
|
||||
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Everyone mentioned in the post' },
|
||||
change_privacy: { id: 'privacy.change', defaultMessage: 'Change post privacy' },
|
||||
unlisted_extra: { id: 'privacy.unlisted.additional', defaultMessage: 'This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.' },
|
||||
});
|
||||
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
|
||||
|
@ -151,6 +152,12 @@ class PrivacyDropdownMenu extends PureComponent {
|
|||
<strong>{item.text}</strong>
|
||||
{item.meta}
|
||||
</div>
|
||||
|
||||
{item.extra && (
|
||||
<div className='privacy-dropdown__option__additional' title={item.extra}>
|
||||
<Icon id='info-circle' icon={item.extraIcomComponent ?? InfoIcon} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
@ -181,30 +188,11 @@ class PrivacyDropdown extends PureComponent {
|
|||
};
|
||||
|
||||
handleToggle = () => {
|
||||
if (this.props.isUserTouching && this.props.isUserTouching()) {
|
||||
if (this.state.open) {
|
||||
this.props.onModalClose();
|
||||
} else {
|
||||
this.props.onModalOpen({
|
||||
actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
|
||||
onClick: this.handleModalActionClick,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ open: !this.state.open });
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
|
||||
handleModalActionClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { value } = this.options[e.currentTarget.getAttribute('data-index')];
|
||||
|
||||
this.props.onModalClose();
|
||||
this.props.onChange(value);
|
||||
this.setState({ open: !this.state.open });
|
||||
};
|
||||
|
||||
handleKeyDown = e => {
|
||||
|
@ -248,10 +236,10 @@ class PrivacyDropdown extends PureComponent {
|
|||
{ icon: 'globe', iconComponent: PublicIcon, value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) },
|
||||
{ icon: 'cloud', iconComponent: PublicUnlistedIcon, value: 'public_unlisted', text: formatMessage(messages.public_unlisted_short), meta: formatMessage(messages.public_unlisted_long) },
|
||||
{ icon: 'key', iconComponent: LoginIcon, value: 'login', text: formatMessage(messages.login_short), meta: formatMessage(messages.login_long) },
|
||||
{ icon: 'unlock', iconComponent: LockOpenIcon, value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) },
|
||||
{ icon: 'unlock', iconComponent: QuietTimeIcon, value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long), extra: formatMessage(messages.unlisted_extra) },
|
||||
{ icon: 'lock', iconComponent: LockIcon, value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) },
|
||||
{ icon: 'exchange', iconComponent: MutualIcon, value: 'mutual', text: formatMessage(messages.mutual_short), meta: formatMessage(messages.mutual_long) },
|
||||
{ icon: 'user-circle', iconComponent: CircleIcon, value: 'circle', text: formatMessage(messages.circle_short), meta: formatMessage(messages.circle_long) },
|
||||
{ icon: 'exchange', iconComponent: MutualIcon, value: 'mutual', text: formatMessage(messages.mutual_short), meta: formatMessage(messages.mutual_long), extra: formatMessage(messages.limited_short), extraIcomComponent: LimitedIcon },
|
||||
{ icon: 'user-circle', iconComponent: CircleIcon, value: 'circle', text: formatMessage(messages.circle_short), meta: formatMessage(messages.circle_long), extra: formatMessage(messages.limited_short), extraIcomComponent: LimitedIcon },
|
||||
];
|
||||
|
||||
if (!this.props.noDirect) {
|
||||
|
@ -294,7 +282,7 @@ class PrivacyDropdown extends PureComponent {
|
|||
if (replyToLimited) {
|
||||
if (!this.selectableOptions.some((op) => op.value === 'reply')) {
|
||||
this.selectableOptions.unshift(
|
||||
{ icon: 'reply', iconComponent: ReplyIcon, value: 'reply', text: intl.formatMessage(messages.reply_short), meta: intl.formatMessage(messages.reply_long) },
|
||||
{ icon: 'reply', iconComponent: ReplyIcon, value: 'reply', text: intl.formatMessage(messages.reply_short), meta: intl.formatMessage(messages.reply_long), extra: intl.formatMessage(messages.limited_short), extraIcomComponent: LimitedIcon },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -307,23 +295,21 @@ class PrivacyDropdown extends PureComponent {
|
|||
|
||||
return (
|
||||
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
|
||||
<IconButton
|
||||
className='privacy-dropdown__value-icon'
|
||||
icon={valueOption.icon}
|
||||
iconComponent={valueOption.iconComponent}
|
||||
<button
|
||||
type='button'
|
||||
title={intl.formatMessage(messages.change_privacy)}
|
||||
size={18}
|
||||
expanded={open}
|
||||
active={open}
|
||||
inverted
|
||||
aria-expanded={open}
|
||||
onClick={this.handleToggle}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
style={{ height: null, lineHeight: '27px' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
className={classNames('dropdown-button', { active: open })}
|
||||
>
|
||||
<Icon id={valueOption.icon} icon={valueOption.iconComponent} />
|
||||
<span className='dropdown-button__label'>{valueOption.text}</span>
|
||||
</button>
|
||||
|
||||
<Overlay show={open} placement={placement} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
<Overlay show={open} offset={[5, 5]} placement={placement} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
||||
|
|
|
@ -1,74 +1,48 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import AttachmentList from 'mastodon/components/attachment_list';
|
||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
|
||||
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
|
||||
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { DisplayName } from 'mastodon/components/display_name';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
export const ReplyIndicator = () => {
|
||||
const inReplyToId = useSelector(state => state.getIn(['compose', 'in_reply_to']));
|
||||
const status = useSelector(state => state.getIn(['statuses', inReplyToId]));
|
||||
const account = useSelector(state => state.getIn(['accounts', status?.get('account')]));
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||
});
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
class ReplyIndicator extends ImmutablePureComponent {
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
...WithOptionalRouterPropTypes,
|
||||
};
|
||||
return (
|
||||
<div className='reply-indicator'>
|
||||
<div className='reply-indicator__line' />
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onCancel();
|
||||
};
|
||||
<Link to={`/@${account.get('acct')}`} className='detailed-status__display-avatar'>
|
||||
<Avatar account={account} size={46} />
|
||||
</Link>
|
||||
|
||||
handleAccountClick = (e) => {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.props.history?.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { status, intl } = this.props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
|
||||
return (
|
||||
<div className='reply-indicator'>
|
||||
<div className='reply-indicator__header'>
|
||||
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={this.handleClick} inverted /></div>
|
||||
|
||||
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='reply-indicator__display-name'>
|
||||
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
</div>
|
||||
<div className='reply-indicator__main'>
|
||||
<Link to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
|
||||
<DisplayName account={account} />
|
||||
</Link>
|
||||
|
||||
<div className='reply-indicator__content translate' dangerouslySetInnerHTML={content} />
|
||||
|
||||
{status.get('media_attachments').size > 0 && (
|
||||
<AttachmentList
|
||||
compact
|
||||
media={status.get('media_attachments')}
|
||||
/>
|
||||
{(status.get('poll') || status.get('media_attachments').size > 0) && (
|
||||
<div className='reply-indicator__attachments'>
|
||||
{status.get('poll') && <><Icon icon={BarChart4BarsIcon} /><FormattedMessage id='reply_indicator.poll' defaultMessage='Poll' /></>}
|
||||
{status.get('media_attachments').size > 0 && <><Icon icon={PhotoLibraryIcon} /><FormattedMessage id='reply_indicator.attachments' defaultMessage='{count, plural, one {# attachment} other {# attachments}}' values={{ count: status.get('media_attachments').size }} /></>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withOptionalRouter(injectIntl(ReplyIndicator));
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,12 +13,9 @@ import PublicUnlistedIcon from '@/material-icons/400-24px/cloud.svg?react';
|
|||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
import LockOpenIcon from '@/material-icons/400-24px/no_encryption.svg?react';
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { enableLocalPrivacy } from 'mastodon/initial_state';
|
||||
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
public_short: { id: 'searchability.public.short', defaultMessage: 'Public' },
|
||||
|
@ -168,30 +165,11 @@ class SearchabilityDropdown extends PureComponent {
|
|||
};
|
||||
|
||||
handleToggle = () => {
|
||||
if (this.props.isUserTouching && this.props.isUserTouching()) {
|
||||
if (this.state.open) {
|
||||
this.props.onModalClose();
|
||||
} else {
|
||||
this.props.onModalOpen({
|
||||
actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })),
|
||||
onClick: this.handleModalActionClick,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ open: !this.state.open });
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
};
|
||||
|
||||
handleModalActionClick = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { value } = this.options[e.currentTarget.getAttribute('data-index')];
|
||||
|
||||
this.props.onModalClose();
|
||||
this.props.onChange(value);
|
||||
this.setState({ open: !this.state.open });
|
||||
};
|
||||
|
||||
handleKeyDown = e => {
|
||||
|
@ -263,31 +241,22 @@ class SearchabilityDropdown extends PureComponent {
|
|||
const valueOption = this.options.find(item => item.value === value);
|
||||
|
||||
return (
|
||||
<div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}>
|
||||
<div className={classNames('privacy-dropdown__value', 'searchability', { active: this.options.indexOf(valueOption) === (placement === 'bottom' ? 0 : (this.options.length - 1)) })} ref={this.setTargetRef}>
|
||||
<IconButton
|
||||
className='privacy-dropdown__value-icon'
|
||||
icon={valueOption.icon}
|
||||
iconComponent={valueOption.iconComponent}
|
||||
title={intl.formatMessage(messages.change_searchability)}
|
||||
size={18}
|
||||
expanded={open}
|
||||
active={open}
|
||||
inverted
|
||||
onClick={this.handleToggle}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
style={{ height: null, lineHeight: '27px' }}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<Icon
|
||||
className='searchability-dropdown__value-overlay'
|
||||
id='search'
|
||||
icon={SearchIcon}
|
||||
/>
|
||||
</div>
|
||||
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
|
||||
<button
|
||||
type='button'
|
||||
title={intl.formatMessage(messages.change_searchability)}
|
||||
aria-expanded={open}
|
||||
onClick={this.handleToggle}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
disabled={disabled}
|
||||
className={classNames('dropdown-button', { active: open })}
|
||||
>
|
||||
<Icon id={valueOption.icon} icon={valueOption.iconComponent} />
|
||||
<span className='dropdown-button__label'>{valueOption.text}</span>
|
||||
</button>
|
||||
|
||||
<Overlay show={open} placement={'bottom'} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
<Overlay show={open} offset={[5, 5]} placement={placement} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
||||
|
|
|
@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
|
|||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
|
@ -9,7 +11,8 @@ import spring from 'react-motion/lib/spring';
|
|||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
||||
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
||||
import { Blurhash } from 'mastodon/components/blurhash';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
|
@ -18,6 +21,7 @@ export default class Upload extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
media: ImmutablePropTypes.map.isRequired,
|
||||
sensitive: PropTypes.bool,
|
||||
onUndo: PropTypes.func.isRequired,
|
||||
onOpenFocalPoint: PropTypes.func.isRequired,
|
||||
};
|
||||
|
@ -33,7 +37,7 @@ export default class Upload extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { media } = this.props;
|
||||
const { media, sensitive } = this.props;
|
||||
|
||||
if (!media) {
|
||||
return null;
|
||||
|
@ -43,22 +47,26 @@ export default class Upload extends ImmutablePureComponent {
|
|||
const focusY = media.getIn(['meta', 'focus', 'y']);
|
||||
const x = ((focusX / 2) + .5) * 100;
|
||||
const y = ((focusY / -2) + .5) * 100;
|
||||
const missingDescription = (media.get('description') || '').length === 0;
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload'>
|
||||
<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}%` }}>
|
||||
<div className='compose-form__upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: !sensitive ? `url(${media.get('preview_url')})` : null, backgroundPosition: `${x}% ${y}%` }}>
|
||||
{sensitive && <Blurhash
|
||||
hash={media.get('blurhash')}
|
||||
className='compose-form__upload__preview'
|
||||
/>}
|
||||
|
||||
<div className='compose-form__upload__actions'>
|
||||
<button type='button' className='icon-button' onClick={this.handleUndoClick}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
|
||||
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
|
||||
<button type='button' className='icon-button compose-form__upload__delete' onClick={this.handleUndoClick}><Icon icon={CloseIcon} /></button>
|
||||
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
|
||||
</div>
|
||||
|
||||
{(media.get('description') || '').length === 0 && (
|
||||
<div className='compose-form__upload__warning'>
|
||||
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' icon={InfoIcon} /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
|
||||
</div>
|
||||
)}
|
||||
<div className='compose-form__upload__warning'>
|
||||
<button type='button' className={classNames('icon-button', { active: missingDescription })} onClick={this.handleFocalPointClick}>{missingDescription && <Icon icon={WarningIcon} />} ALT</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Motion>
|
||||
|
|
|
@ -6,9 +6,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
|||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import AddPhotoAlternateIcon from '@/material-icons/400-24px/add_photo_alternate.svg?react';
|
||||
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
upload: { id: 'upload_button.label', defaultMessage: 'Add images, a video or an audio file' },
|
||||
|
@ -31,7 +30,6 @@ class UploadButton extends ImmutablePureComponent {
|
|||
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
unavailable: PropTypes.bool,
|
||||
onSelectFile: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
resetFileKey: PropTypes.number,
|
||||
|
@ -54,17 +52,13 @@ class UploadButton extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props;
|
||||
|
||||
if (unavailable) {
|
||||
return null;
|
||||
}
|
||||
const { intl, resetFileKey, disabled, acceptContentTypes } = this.props;
|
||||
|
||||
const message = intl.formatMessage(messages.upload);
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload-button'>
|
||||
<IconButton icon='paperclip' iconComponent={AddPhotoAlternateIcon} title={message} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
|
||||
<IconButton icon='paperclip' iconComponent={PhotoLibraryIcon} title={message} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{message}</span>
|
||||
<input
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import SensitiveButtonContainer from '../containers/sensitive_button_container';
|
||||
import UploadContainer from '../containers/upload_container';
|
||||
import UploadProgressContainer from '../containers/upload_progress_container';
|
||||
|
||||
|
@ -15,17 +14,17 @@ export default class UploadForm extends ImmutablePureComponent {
|
|||
const { mediaIds } = this.props;
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload-wrapper'>
|
||||
<>
|
||||
<UploadProgressContainer />
|
||||
|
||||
<div className='compose-form__uploads-wrapper'>
|
||||
{mediaIds.map(id => (
|
||||
<UploadContainer id={id} key={id} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!mediaIds.isEmpty() && <SensitiveButtonContainer />}
|
||||
</div>
|
||||
{mediaIds.size > 0 && (
|
||||
<div className='compose-form__uploads'>
|
||||
{mediaIds.map(id => (
|
||||
<UploadContainer id={id} key={id} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -35,9 +35,7 @@ export default class UploadProgress extends PureComponent {
|
|||
|
||||
return (
|
||||
<div className='upload-progress'>
|
||||
<div className='upload-progress__icon'>
|
||||
<Icon id='upload' icon={UploadFileIcon} />
|
||||
</div>
|
||||
<Icon id='upload' icon={UploadFileIcon} />
|
||||
|
||||
<div className='upload-progress__message'>
|
||||
{message}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeCircle } from '../../../actions/compose';
|
||||
import CircleSelect from '../components/circle_select';
|
||||
import { openModal, closeModal } from '../../../actions/modal';
|
||||
import { isUserTouching } from '../../../is_mobile';
|
||||
import CircleDropdown from '../components/circle_dropdown';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
unavailable: state.getIn(['compose', 'privacy']) !== 'circle' || !!state.getIn(['compose', 'id']),
|
||||
value: state.getIn(['compose', 'searchability']),
|
||||
circles: state.get('circles'),
|
||||
circleId: state.getIn(['compose', 'circle_id']),
|
||||
});
|
||||
|
@ -15,6 +18,16 @@ const mapDispatchToProps = dispatch => ({
|
|||
dispatch(changeCircle(circleId));
|
||||
},
|
||||
|
||||
isUserTouching,
|
||||
onModalOpen: props => dispatch(openModal({
|
||||
modalType: 'ACTIONS',
|
||||
modalProps: props,
|
||||
})),
|
||||
onModalClose: () => dispatch(closeModal({
|
||||
modalType: undefined,
|
||||
ignoreFocus: false,
|
||||
})),
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CircleSelect);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CircleDropdown);
|
|
@ -4,8 +4,7 @@ import { openModal, closeModal } from '../../../actions/modal';
|
|||
import { isUserTouching } from '../../../is_mobile';
|
||||
import ExpirationDropdown from '../components/expiration_dropdown';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
value: state.getIn(['compose', 'privacy']),
|
||||
const mapStateToProps = () => ({
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { onPickExpiration }) => ({
|
||||
|
|
|
@ -2,8 +2,10 @@ import { injectIntl, defineMessages } from 'react-intl';
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import MarkdownIcon from '@/material-icons/400-24px/markdown.svg?react';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
|
||||
import { changeComposeMarkdown } from '../../../actions/compose';
|
||||
import TextIconButton from '../components/text_icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: { id: 'compose_form.markdown.marked', defaultMessage: 'Markdown is enabled' },
|
||||
|
@ -11,10 +13,12 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const mapStateToProps = (state, { intl }) => ({
|
||||
label: 'MD',
|
||||
iconComponent: MarkdownIcon,
|
||||
title: intl.formatMessage(state.getIn(['compose', 'markdown']) ? messages.marked : messages.unmarked),
|
||||
active: state.getIn(['compose', 'markdown']),
|
||||
ariaControls: 'cw-markdown-input',
|
||||
size: 18,
|
||||
inverted: true,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
@ -25,4 +29,4 @@ const mapDispatchToProps = dispatch => ({
|
|||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TextIconButton));
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(IconButton));
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { logOut } from 'mastodon/utils/log_out';
|
||||
|
||||
import { me } from '../../../initial_state';
|
||||
import NavigationBar from '../components/navigation_bar';
|
||||
|
||||
const messages = defineMessages({
|
||||
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
||||
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
account: state.getIn(['accounts', me]),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onLogout () {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.logoutMessage),
|
||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||
closeWhenConfirm: false,
|
||||
onConfirm: () => logOut(),
|
||||
},
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NavigationBar));
|
|
@ -4,7 +4,7 @@ import { addPoll, removePoll } from '../../../actions/compose';
|
|||
import PollButton from '../components/poll_button';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
unavailable: false,
|
||||
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0),
|
||||
active: state.getIn(['compose', 'poll']) !== null,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
addPollOption,
|
||||
removePollOption,
|
||||
changePollOption,
|
||||
changePollSettings,
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
selectComposeSuggestion,
|
||||
} from '../../../actions/compose';
|
||||
import PollForm from '../components/poll_form';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
suggestions: state.getIn(['compose', 'suggestions']),
|
||||
options: state.getIn(['compose', 'poll', 'options']),
|
||||
lang: state.getIn(['compose', 'language']),
|
||||
expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
|
||||
isMultiple: state.getIn(['compose', 'poll', 'multiple']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onAddOption(title) {
|
||||
dispatch(addPollOption(title));
|
||||
},
|
||||
|
||||
onRemoveOption(index) {
|
||||
dispatch(removePollOption(index));
|
||||
},
|
||||
|
||||
onChangeOption(index, title) {
|
||||
dispatch(changePollOption(index, title));
|
||||
},
|
||||
|
||||
onChangeSettings(expiresIn, isMultiple) {
|
||||
dispatch(changePollSettings(expiresIn, isMultiple));
|
||||
},
|
||||
|
||||
onClearSuggestions () {
|
||||
dispatch(clearComposeSuggestions());
|
||||
},
|
||||
|
||||
onFetchSuggestions (token) {
|
||||
dispatch(fetchComposeSuggestions(token));
|
||||
},
|
||||
|
||||
onSuggestionSelected (position, token, accountId, path) {
|
||||
dispatch(selectComposeSuggestion(position, token, accountId, path));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PollForm);
|
|
@ -1,36 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { cancelReplyCompose } from '../../../actions/compose';
|
||||
import { makeGetStatus } from '../../../selectors';
|
||||
import ReplyIndicator from '../components/reply_indicator';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const mapStateToProps = state => {
|
||||
let statusId = state.getIn(['compose', 'id'], null);
|
||||
let editing = true;
|
||||
|
||||
if (statusId === null) {
|
||||
statusId = state.getIn(['compose', 'in_reply_to']);
|
||||
editing = false;
|
||||
}
|
||||
|
||||
return {
|
||||
status: getStatus(state, { id: statusId }),
|
||||
editing,
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onCancel () {
|
||||
dispatch(cancelReplyCompose());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator);
|
|
@ -1,73 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeComposeSensitivity } from 'mastodon/actions/compose';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: {
|
||||
id: 'compose_form.sensitive.marked',
|
||||
defaultMessage: '{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}',
|
||||
},
|
||||
unmarked: {
|
||||
id: 'compose_form.sensitive.unmarked',
|
||||
defaultMessage: '{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}',
|
||||
},
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
active: state.getIn(['compose', 'sensitive']),
|
||||
disabled: state.getIn(['compose', 'spoiler']),
|
||||
mediaCount: state.getIn(['compose', 'media_attachments']).size,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onClick () {
|
||||
dispatch(changeComposeSensitivity());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
class SensitiveButton extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
mediaCount: PropTypes.number,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { active, disabled, mediaCount, onClick, intl } = this.props;
|
||||
|
||||
return (
|
||||
<div className='compose-form__sensitive-button'>
|
||||
<label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked, { count: mediaCount })}>
|
||||
<input
|
||||
name='mark-sensitive'
|
||||
type='checkbox'
|
||||
checked={active}
|
||||
onChange={onClick}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<FormattedMessage
|
||||
id='compose_form.sensitive.hide'
|
||||
defaultMessage='{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}'
|
||||
values={{ count: mediaCount }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton));
|
|
@ -2,8 +2,10 @@ import { injectIntl, defineMessages } from 'react-intl';
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import WarningIcon from 'mastodon/../material-icons/400-24px/warning.svg?react';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
|
||||
import { changeComposeSpoilerness } from '../../../actions/compose';
|
||||
import TextIconButton from '../components/text_icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' },
|
||||
|
@ -11,10 +13,12 @@ const messages = defineMessages({
|
|||
});
|
||||
|
||||
const mapStateToProps = (state, { intl }) => ({
|
||||
label: 'CW',
|
||||
iconComponent: WarningIcon,
|
||||
title: intl.formatMessage(state.getIn(['compose', 'spoiler']) ? messages.marked : messages.unmarked),
|
||||
active: state.getIn(['compose', 'spoiler']),
|
||||
ariaControls: 'cw-spoiler-input',
|
||||
size: 18,
|
||||
inverted: true,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
@ -25,4 +29,4 @@ const mapDispatchToProps = dispatch => ({
|
|||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TextIconButton));
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(IconButton));
|
||||
|
|
|
@ -4,8 +4,7 @@ import { uploadCompose } from '../../../actions/compose';
|
|||
import UploadButton from '../components/upload_button';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) >= 4 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
|
||||
unavailable: false,
|
||||
disabled: state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
});
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import Upload from '../components/upload';
|
|||
|
||||
const mapStateToProps = (state, { id }) => ({
|
||||
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
|
||||
sensitive: state.getIn(['compose', 'spoiler']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
|
|
@ -30,7 +30,6 @@ import { isMobile } from '../../is_mobile';
|
|||
import Motion from '../ui/util/optional_motion';
|
||||
|
||||
import ComposeFormContainer from './containers/compose_form_container';
|
||||
import NavigationContainer from './containers/navigation_container';
|
||||
import SearchContainer from './containers/search_container';
|
||||
import SearchResultsContainer from './containers/search_results_container';
|
||||
|
||||
|
@ -129,8 +128,6 @@ class Compose extends PureComponent {
|
|||
|
||||
<div className='drawer__pager'>
|
||||
<div className='drawer__inner' onFocus={this.onFocus}>
|
||||
<NavigationContainer onClose={this.onBlur} />
|
||||
|
||||
<ComposeFormContainer autoFocus={!isMobile(window.innerWidth)} />
|
||||
|
||||
<div className='drawer__inner__mastodon'>
|
||||
|
@ -152,7 +149,6 @@ class Compose extends PureComponent {
|
|||
|
||||
return (
|
||||
<Column onFocus={this.onFocus}>
|
||||
<NavigationContainer onClose={this.onBlur} />
|
||||
<ComposeFormContainer />
|
||||
|
||||
<Helmet>
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||
import { replyCompose } from 'mastodon/actions/compose';
|
||||
import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { muteStatus, unmuteStatus, revealStatus, hideStatus } from 'mastodon/actions/statuses';
|
||||
import AttachmentList from 'mastodon/components/attachment_list';
|
||||
import AvatarComposite from 'mastodon/components/avatar_composite';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
|
@ -19,7 +26,7 @@ import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
|||
import StatusContent from 'mastodon/components/status_content';
|
||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
|
||||
import { autoPlayGif } from 'mastodon/initial_state';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
import { makeGetStatus } from 'mastodon/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
more: { id: 'status.more', defaultMessage: 'More' },
|
||||
|
@ -29,25 +36,31 @@ const messages = defineMessages({
|
|||
delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
|
||||
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
|
||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
});
|
||||
|
||||
class Conversation extends ImmutablePureComponent {
|
||||
const getAccounts = createSelector(
|
||||
(state) => state.get('accounts'),
|
||||
(_, accountIds) => accountIds,
|
||||
(accounts, accountIds) =>
|
||||
accountIds.map(id => accounts.get(id))
|
||||
);
|
||||
|
||||
static propTypes = {
|
||||
conversationId: PropTypes.string.isRequired,
|
||||
accounts: ImmutablePropTypes.list.isRequired,
|
||||
lastStatus: ImmutablePropTypes.map,
|
||||
unread:PropTypes.bool.isRequired,
|
||||
scrollKey: PropTypes.string,
|
||||
onMoveUp: PropTypes.func,
|
||||
onMoveDown: PropTypes.func,
|
||||
markRead: PropTypes.func.isRequired,
|
||||
delete: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
...WithRouterPropTypes,
|
||||
};
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
handleMouseEnter = ({ currentTarget }) => {
|
||||
export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown }) => {
|
||||
const id = conversation.get('id');
|
||||
const unread = conversation.get('unread');
|
||||
const lastStatusId = conversation.get('last_status');
|
||||
const accountIds = conversation.get('accounts');
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const lastStatus = useSelector(state => getStatus(state, { id: lastStatusId }));
|
||||
const accounts = useSelector(state => getAccounts(state, accountIds));
|
||||
|
||||
const handleMouseEnter = useCallback(({ currentTarget }) => {
|
||||
if (autoPlayGif) {
|
||||
return;
|
||||
}
|
||||
|
@ -58,9 +71,9 @@ class Conversation extends ImmutablePureComponent {
|
|||
let emoji = emojis[i];
|
||||
emoji.src = emoji.getAttribute('data-original');
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
handleMouseLeave = ({ currentTarget }) => {
|
||||
const handleMouseLeave = useCallback(({ currentTarget }) => {
|
||||
if (autoPlayGif) {
|
||||
return;
|
||||
}
|
||||
|
@ -71,136 +84,161 @@ class Conversation extends ImmutablePureComponent {
|
|||
let emoji = emojis[i];
|
||||
emoji.src = emoji.getAttribute('data-static');
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
if (!this.props.history) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { lastStatus, unread, markRead } = this.props;
|
||||
}, []);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (unread) {
|
||||
markRead();
|
||||
dispatch(markConversationRead(id));
|
||||
}
|
||||
|
||||
this.props.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
|
||||
};
|
||||
history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
|
||||
}, [dispatch, history, unread, id, lastStatus]);
|
||||
|
||||
handleMarkAsRead = () => {
|
||||
this.props.markRead();
|
||||
};
|
||||
const handleMarkAsRead = useCallback(() => {
|
||||
dispatch(markConversationRead(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
handleReply = () => {
|
||||
this.props.reply(this.props.lastStatus, this.props.history);
|
||||
};
|
||||
const handleReply = useCallback(() => {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
|
||||
handleDelete = () => {
|
||||
this.props.delete();
|
||||
};
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
onConfirm: () => dispatch(replyCompose(lastStatus, history)),
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
dispatch(replyCompose(lastStatus, history));
|
||||
}
|
||||
});
|
||||
}, [dispatch, lastStatus, history, intl]);
|
||||
|
||||
handleHotkeyMoveUp = () => {
|
||||
this.props.onMoveUp(this.props.conversationId);
|
||||
};
|
||||
const handleDelete = useCallback(() => {
|
||||
dispatch(deleteConversation(id));
|
||||
}, [dispatch, id]);
|
||||
|
||||
handleHotkeyMoveDown = () => {
|
||||
this.props.onMoveDown(this.props.conversationId);
|
||||
};
|
||||
const handleHotkeyMoveUp = useCallback(() => {
|
||||
onMoveUp(id);
|
||||
}, [id, onMoveUp]);
|
||||
|
||||
handleConversationMute = () => {
|
||||
this.props.onMute(this.props.lastStatus);
|
||||
};
|
||||
const handleHotkeyMoveDown = useCallback(() => {
|
||||
onMoveDown(id);
|
||||
}, [id, onMoveDown]);
|
||||
|
||||
handleShowMore = () => {
|
||||
this.props.onToggleHidden(this.props.lastStatus);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { accounts, lastStatus, unread, scrollKey, intl } = this.props;
|
||||
|
||||
if (lastStatus === null) {
|
||||
return null;
|
||||
const handleConversationMute = useCallback(() => {
|
||||
if (lastStatus.get('muted')) {
|
||||
dispatch(unmuteStatus(lastStatus.get('id')));
|
||||
} else {
|
||||
dispatch(muteStatus(lastStatus.get('id')));
|
||||
}
|
||||
}, [dispatch, lastStatus]);
|
||||
|
||||
const menu = [
|
||||
{ text: intl.formatMessage(messages.open), action: this.handleClick },
|
||||
null,
|
||||
];
|
||||
|
||||
menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute });
|
||||
|
||||
if (unread) {
|
||||
menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead });
|
||||
menu.push(null);
|
||||
const handleShowMore = useCallback(() => {
|
||||
if (lastStatus.get('hidden')) {
|
||||
dispatch(revealStatus(lastStatus.get('id')));
|
||||
} else {
|
||||
dispatch(hideStatus(lastStatus.get('id')));
|
||||
}
|
||||
}, [dispatch, lastStatus]);
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete });
|
||||
if (!lastStatus) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const names = accounts.map(a => <Link to={`/@${a.get('acct')}`} key={a.get('id')} title={a.get('acct')}><bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }} /></bdi></Link>).reduce((prev, cur) => [prev, ', ', cur]);
|
||||
const menu = [
|
||||
{ text: intl.formatMessage(messages.open), action: handleClick },
|
||||
null,
|
||||
{ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: handleConversationMute },
|
||||
];
|
||||
|
||||
const handlers = {
|
||||
reply: this.handleReply,
|
||||
open: this.handleClick,
|
||||
moveUp: this.handleHotkeyMoveUp,
|
||||
moveDown: this.handleHotkeyMoveDown,
|
||||
toggleHidden: this.handleShowMore,
|
||||
};
|
||||
if (unread) {
|
||||
menu.push({ text: intl.formatMessage(messages.markAsRead), action: handleMarkAsRead });
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<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>
|
||||
menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
|
||||
|
||||
<div className='conversation__content'>
|
||||
<div className='conversation__content__info'>
|
||||
<div className='conversation__content__relative-time'>
|
||||
{unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
|
||||
</div>
|
||||
const names = accounts.map(a => (
|
||||
<Link to={`/@${a.get('acct')}`} key={a.get('id')} title={a.get('acct')}>
|
||||
<bdi>
|
||||
<strong
|
||||
className='display-name__html'
|
||||
dangerouslySetInnerHTML={{ __html: a.get('display_name_html') }}
|
||||
/>
|
||||
</bdi>
|
||||
</Link>
|
||||
)).reduce((prev, cur) => [prev, ', ', cur]);
|
||||
|
||||
<div className='conversation__content__names' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
<FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
|
||||
</div>
|
||||
const handlers = {
|
||||
reply: handleReply,
|
||||
open: handleClick,
|
||||
moveUp: handleHotkeyMoveUp,
|
||||
moveDown: handleHotkeyMoveDown,
|
||||
toggleHidden: handleShowMore,
|
||||
};
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex={0}>
|
||||
<div className='conversation__avatar' onClick={handleClick} role='presentation'>
|
||||
<AvatarComposite accounts={accounts} size={48} />
|
||||
</div>
|
||||
|
||||
<div className='conversation__content'>
|
||||
<div className='conversation__content__info'>
|
||||
<div className='conversation__content__relative-time'>
|
||||
{unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
|
||||
</div>
|
||||
|
||||
<StatusContent
|
||||
status={lastStatus}
|
||||
onClick={this.handleClick}
|
||||
expanded={!lastStatus.get('hidden')}
|
||||
onExpandedToggle={this.handleShowMore}
|
||||
collapsible
|
||||
<div className='conversation__content__names' onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
<FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatusContent
|
||||
status={lastStatus}
|
||||
onClick={handleClick}
|
||||
expanded={!lastStatus.get('hidden')}
|
||||
onExpandedToggle={handleShowMore}
|
||||
collapsible
|
||||
/>
|
||||
|
||||
{lastStatus.get('media_attachments').size > 0 && (
|
||||
<AttachmentList
|
||||
compact
|
||||
media={lastStatus.get('media_attachments')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{lastStatus.get('media_attachments').size > 0 && (
|
||||
<AttachmentList
|
||||
compact
|
||||
media={lastStatus.get('media_attachments')}
|
||||
<div className='status__action-bar'>
|
||||
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' iconComponent={ReplyIcon} onClick={handleReply} />
|
||||
|
||||
<div className='status__action-bar-dropdown'>
|
||||
<DropdownMenuContainer
|
||||
scrollKey={scrollKey}
|
||||
status={lastStatus}
|
||||
items={menu}
|
||||
icon='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
size={18}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className='status__action-bar'>
|
||||
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' iconComponent={ReplyIcon} onClick={this.handleReply} />
|
||||
|
||||
<div className='status__action-bar-dropdown'>
|
||||
<DropdownMenuContainer
|
||||
scrollKey={scrollKey}
|
||||
status={lastStatus}
|
||||
items={menu}
|
||||
icon='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
size={18}
|
||||
direction='right'
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export default withRouter(injectIntl(Conversation));
|
||||
Conversation.propTypes = {
|
||||
conversation: ImmutablePropTypes.map.isRequired,
|
||||
scrollKey: PropTypes.string,
|
||||
onMoveUp: PropTypes.func,
|
||||
onMoveDown: PropTypes.func,
|
||||
};
|
||||
|
|
|
@ -1,77 +1,72 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useRef, useMemo, useCallback } from 'react';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import ScrollableList from '../../../components/scrollable_list';
|
||||
import ConversationContainer from '../containers/conversation_container';
|
||||
import { expandConversations } from 'mastodon/actions/conversations';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
|
||||
export default class ConversationsList extends ImmutablePureComponent {
|
||||
import { Conversation } from './conversation';
|
||||
|
||||
static propTypes = {
|
||||
conversations: ImmutablePropTypes.list.isRequired,
|
||||
scrollKey: PropTypes.string.isRequired,
|
||||
hasMore: PropTypes.bool,
|
||||
isLoading: PropTypes.bool,
|
||||
onLoadMore: PropTypes.func,
|
||||
};
|
||||
const focusChild = (node, index, alignTop) => {
|
||||
const element = node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id);
|
||||
|
||||
handleMoveUp = id => {
|
||||
const elementIndex = this.getCurrentIndex(id) - 1;
|
||||
this._selectChild(elementIndex, true);
|
||||
};
|
||||
|
||||
handleMoveDown = id => {
|
||||
const elementIndex = this.getCurrentIndex(id) + 1;
|
||||
this._selectChild(elementIndex, false);
|
||||
};
|
||||
|
||||
_selectChild (index, align_top) {
|
||||
const container = this.node.node;
|
||||
const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
|
||||
|
||||
if (element) {
|
||||
if (align_top && container.scrollTop > element.offsetTop) {
|
||||
element.scrollIntoView(true);
|
||||
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
element.focus();
|
||||
if (element) {
|
||||
if (alignTop && node.scrollTop > element.offsetTop) {
|
||||
element.scrollIntoView(true);
|
||||
} else if (!alignTop && node.scrollTop + node.clientHeight < element.offsetTop + element.offsetHeight) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
|
||||
element.focus();
|
||||
}
|
||||
};
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
};
|
||||
export const ConversationsList = ({ scrollKey, ...other }) => {
|
||||
const listRef = useRef();
|
||||
const conversations = useSelector(state => state.getIn(['conversations', 'items']));
|
||||
const isLoading = useSelector(state => state.getIn(['conversations', 'isLoading'], true));
|
||||
const hasMore = useSelector(state => state.getIn(['conversations', 'hasMore'], false));
|
||||
const dispatch = useDispatch();
|
||||
const lastStatusId = conversations.last()?.get('last_status');
|
||||
|
||||
handleLoadOlder = debounce(() => {
|
||||
const last = this.props.conversations.last();
|
||||
const handleMoveUp = useCallback(id => {
|
||||
const elementIndex = conversations.findIndex(x => x.get('id') === id) - 1;
|
||||
focusChild(listRef.current.node, elementIndex, true);
|
||||
}, [listRef, conversations]);
|
||||
|
||||
if (last && last.get('last_status')) {
|
||||
this.props.onLoadMore(last.get('last_status'));
|
||||
const handleMoveDown = useCallback(id => {
|
||||
const elementIndex = conversations.findIndex(x => x.get('id') === id) + 1;
|
||||
focusChild(listRef.current.node, elementIndex, false);
|
||||
}, [listRef, conversations]);
|
||||
|
||||
const debouncedLoadMore = useMemo(() => debounce(id => {
|
||||
dispatch(expandConversations({ maxId: id }));
|
||||
}, 300, { leading: true }), [dispatch]);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
if (lastStatusId) {
|
||||
debouncedLoadMore(lastStatusId);
|
||||
}
|
||||
}, 300, { leading: true });
|
||||
}, [debouncedLoadMore, lastStatusId]);
|
||||
|
||||
render () {
|
||||
const { conversations, isLoading, onLoadMore, ...other } = this.props;
|
||||
return (
|
||||
<ScrollableList {...other} scrollKey={scrollKey} isLoading={isLoading} showLoading={isLoading && conversations.isEmpty()} hasMore={hasMore} onLoadMore={handleLoadMore} ref={listRef}>
|
||||
{conversations.map(item => (
|
||||
<Conversation
|
||||
key={item.get('id')}
|
||||
conversation={item}
|
||||
onMoveUp={handleMoveUp}
|
||||
onMoveDown={handleMoveDown}
|
||||
scrollKey={scrollKey}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollableList {...other} isLoading={isLoading} showLoading={isLoading && conversations.isEmpty()} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
|
||||
{conversations.map(item => (
|
||||
<ConversationContainer
|
||||
key={item.get('id')}
|
||||
conversationId={item.get('id')}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
scrollKey={this.props.scrollKey}
|
||||
/>
|
||||
))}
|
||||
</ScrollableList>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
ConversationsList.propTypes = {
|
||||
scrollKey: PropTypes.string.isRequired,
|
||||
};
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { replyCompose } from 'mastodon/actions/compose';
|
||||
import { markConversationRead, deleteConversation } from 'mastodon/actions/conversations';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { muteStatus, unmuteStatus, hideStatus, revealStatus } from 'mastodon/actions/statuses';
|
||||
import { makeGetStatus } from 'mastodon/selectors';
|
||||
|
||||
import Conversation from '../components/conversation';
|
||||
|
||||
const messages = defineMessages({
|
||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
});
|
||||
|
||||
const mapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
return (state, { conversationId }) => {
|
||||
const conversation = state.getIn(['conversations', 'items']).find(x => x.get('id') === conversationId);
|
||||
const lastStatusId = conversation.get('last_status', null);
|
||||
|
||||
return {
|
||||
accounts: conversation.get('accounts').map(accountId => state.getIn(['accounts', accountId], null)),
|
||||
unread: conversation.get('unread'),
|
||||
lastStatus: lastStatusId && getStatus(state, { id: lastStatusId }),
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl, conversationId }) => ({
|
||||
|
||||
markRead () {
|
||||
dispatch(markConversationRead(conversationId));
|
||||
},
|
||||
|
||||
reply (status, router) {
|
||||
dispatch((_, getState) => {
|
||||
let state = getState();
|
||||
|
||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
onConfirm: () => dispatch(replyCompose(status, router)),
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
dispatch(replyCompose(status, router));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
delete () {
|
||||
dispatch(deleteConversation(conversationId));
|
||||
},
|
||||
|
||||
onMute (status) {
|
||||
if (status.get('muted')) {
|
||||
dispatch(unmuteStatus(status.get('id')));
|
||||
} else {
|
||||
dispatch(muteStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
onToggleHidden (status) {
|
||||
if (status.get('hidden')) {
|
||||
dispatch(revealStatus(status.get('id')));
|
||||
} else {
|
||||
dispatch(hideStatus(status.get('id')));
|
||||
}
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Conversation));
|
|
@ -1,16 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { expandConversations } from '../../../actions/conversations';
|
||||
import ConversationsList from '../components/conversations_list';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
conversations: state.getIn(['conversations', 'items']),
|
||||
isLoading: state.getIn(['conversations', 'isLoading'], true),
|
||||
hasMore: state.getIn(['conversations', 'hasMore'], false),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onLoadMore: maxId => dispatch(expandConversations({ maxId })),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ConversationsList);
|
|
@ -1,11 +1,11 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
import { useRef, useCallback, useEffect } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
|
||||
|
@ -14,103 +14,79 @@ import { connectDirectStream } from 'mastodon/actions/streaming';
|
|||
import Column from 'mastodon/components/column';
|
||||
import ColumnHeader from 'mastodon/components/column_header';
|
||||
|
||||
import ConversationsListContainer from './containers/conversations_list_container';
|
||||
import { ConversationsList } from './components/conversations_list';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.direct', defaultMessage: 'Private mentions' },
|
||||
});
|
||||
|
||||
class DirectTimeline extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
columnId: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasUnread: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
handlePin = () => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
const DirectTimeline = ({ columnId, multiColumn }) => {
|
||||
const columnRef = useRef();
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const pinned = !!columnId;
|
||||
|
||||
const handlePin = useCallback(() => {
|
||||
if (columnId) {
|
||||
dispatch(removeColumn(columnId));
|
||||
} else {
|
||||
dispatch(addColumn('DIRECT', {}));
|
||||
}
|
||||
};
|
||||
}, [dispatch, columnId]);
|
||||
|
||||
handleMove = (dir) => {
|
||||
const { columnId, dispatch } = this.props;
|
||||
const handleMove = useCallback((dir) => {
|
||||
dispatch(moveColumn(columnId, dir));
|
||||
};
|
||||
}, [dispatch, columnId]);
|
||||
|
||||
handleHeaderClick = () => {
|
||||
this.column.scrollTop();
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
const { dispatch } = this.props;
|
||||
const handleHeaderClick = useCallback(() => {
|
||||
columnRef.current.scrollTop();
|
||||
}, [columnRef]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(mountConversations());
|
||||
dispatch(expandConversations());
|
||||
this.disconnect = dispatch(connectDirectStream());
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.props.dispatch(unmountConversations());
|
||||
const disconnect = dispatch(connectDirectStream());
|
||||
|
||||
if (this.disconnect) {
|
||||
this.disconnect();
|
||||
this.disconnect = null;
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
dispatch(unmountConversations());
|
||||
disconnect();
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
setRef = c => {
|
||||
this.column = c;
|
||||
};
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
icon='at'
|
||||
iconComponent={AlternateEmailIcon}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
onPin={handlePin}
|
||||
onMove={handleMove}
|
||||
onClick={handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
/>
|
||||
|
||||
handleLoadMore = maxId => {
|
||||
this.props.dispatch(expandConversations({ maxId }));
|
||||
};
|
||||
<ConversationsList
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`direct_timeline-${columnId}`}
|
||||
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." />}
|
||||
bindToDocument={!multiColumn}
|
||||
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>}
|
||||
alwaysPrepend
|
||||
/>
|
||||
|
||||
render () {
|
||||
const { intl, hasUnread, columnId, multiColumn } = this.props;
|
||||
const pinned = !!columnId;
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
icon='at'
|
||||
iconComponent={AlternateEmailIcon}
|
||||
active={hasUnread}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
onPin={this.handlePin}
|
||||
onMove={this.handleMove}
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
/>
|
||||
DirectTimeline.propTypes = {
|
||||
columnId: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
<ConversationsListContainer
|
||||
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>}
|
||||
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>
|
||||
<title>{intl.formatMessage(messages.title)}</title>
|
||||
<meta name='robots' content='noindex' />
|
||||
</Helmet>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect()(injectIntl(DirectTimeline));
|
||||
export default DirectTimeline;
|
||||
|
|
|
@ -28,7 +28,7 @@ import ColumnHeader from 'mastodon/components/column_header';
|
|||
import LinkFooter from 'mastodon/features/ui/components/link_footer';
|
||||
|
||||
import { me, showTrends } from '../../initial_state';
|
||||
import NavigationContainer from '../compose/containers/navigation_container';
|
||||
import { NavigationBar } from '../compose/components/navigation_bar';
|
||||
import ColumnLink from '../ui/components/column_link';
|
||||
import ColumnSubheading from '../ui/components/column_subheading';
|
||||
|
||||
|
@ -149,7 +149,7 @@ class GettingStarted extends ImmutablePureComponent {
|
|||
|
||||
return (
|
||||
<Column>
|
||||
{(signedIn && !multiColumn) ? <NavigationContainer /> : <ColumnHeader title={intl.formatMessage(messages.menu)} icon='bars' iconComponent={MenuIcon} multiColumn={multiColumn} />}
|
||||
{(signedIn && !multiColumn) ? <NavigationBar /> : <ColumnHeader title={intl.formatMessage(messages.menu)} icon='bars' iconComponent={MenuIcon} multiColumn={multiColumn} />}
|
||||
|
||||
<div className='getting-started scrollable scrollable--flex'>
|
||||
<div className='getting-started__wrapper'>
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
import { PureComponent } from 'react';
|
||||
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
|
||||
import LoadingBarContainer from 'mastodon/features/ui/containers/loading_bar_container';
|
||||
import ModalContainer from 'mastodon/features/ui/containers/modal_container';
|
||||
import NotificationsContainer from 'mastodon/features/ui/containers/notifications_container';
|
||||
|
||||
import ComposeFormContainer from '../../compose/containers/compose_form_container';
|
||||
import LoadingBarContainer from '../../ui/containers/loading_bar_container';
|
||||
import ModalContainer from '../../ui/containers/modal_container';
|
||||
import NotificationsContainer from '../../ui/containers/notifications_container';
|
||||
const Compose = () => (
|
||||
<>
|
||||
<ComposeFormContainer autoFocus withoutNavigation />
|
||||
<NotificationsContainer />
|
||||
<ModalContainer />
|
||||
<LoadingBarContainer className='loading-bar' />
|
||||
</>
|
||||
);
|
||||
|
||||
export default class Compose extends PureComponent {
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<ComposeFormContainer autoFocus />
|
||||
<NotificationsContainer />
|
||||
<ModalContainer />
|
||||
<LoadingBarContainer className='loading-bar' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
export default Compose;
|
||||
|
|
|
@ -6,7 +6,6 @@ import { connect } from 'react-redux';
|
|||
import { changeComposing, mountCompose, unmountCompose } from 'mastodon/actions/compose';
|
||||
import ServerBanner from 'mastodon/components/server_banner';
|
||||
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container';
|
||||
import NavigationContainer from 'mastodon/features/compose/containers/navigation_container';
|
||||
import SearchContainer from 'mastodon/features/compose/containers/search_container';
|
||||
|
||||
import LinkFooter from './link_footer';
|
||||
|
@ -56,10 +55,7 @@ class ComposePanel extends PureComponent {
|
|||
)}
|
||||
|
||||
{signedIn && (
|
||||
<>
|
||||
<NavigationContainer onClose={this.onBlur} />
|
||||
<ComposeFormContainer singleColumn />
|
||||
</>
|
||||
<ComposeFormContainer singleColumn />
|
||||
)}
|
||||
|
||||
<LinkFooter />
|
||||
|
|
|
@ -21,7 +21,7 @@ import { Button } from 'mastodon/components/button';
|
|||
import { GIFV } from 'mastodon/components/gifv';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import Audio from 'mastodon/features/audio';
|
||||
import CharacterCounter from 'mastodon/features/compose/components/character_counter';
|
||||
import { CharacterCounter } from 'mastodon/features/compose/components/character_counter';
|
||||
import UploadProgress from 'mastodon/features/compose/components/upload_progress';
|
||||
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
|
|
|
@ -108,7 +108,6 @@ class MuteModal extends PureComponent {
|
|||
<div>
|
||||
<span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span>
|
||||
|
||||
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
|
||||
<select value={muteDuration} onChange={this.changeMuteDuration}>
|
||||
<option value={0}>{intl.formatMessage(messages.indefinite)}</option>
|
||||
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
|
||||
|
|
|
@ -94,7 +94,6 @@ class NavigationPanel extends Component {
|
|||
<div className='navigation-panel'>
|
||||
<div className='navigation-panel__logo'>
|
||||
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
|
||||
{!banner && <hr />}
|
||||
</div>
|
||||
|
||||
{banner &&
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
"about.contact": "Kontak:",
|
||||
"about.disclaimer": "Mastodon is gratis oopbronsagteware en ’n handelsmerk van Mastodon gGmbH.",
|
||||
"about.domain_blocks.no_reason_available": "Rede nie beskikbaar nie",
|
||||
"about.domain_blocks.preamble": "Mastodon generally allows you to view content from and interact with users from any other server in the fediverse. These are the exceptions that have been made on this particular server.",
|
||||
"about.domain_blocks.silenced.title": "Beperk",
|
||||
"about.domain_blocks.suspended.title": "Opgeskort",
|
||||
"about.not_available": "Hierdie inligting is nie op hierdie bediener beskikbaar gestel nie.",
|
||||
|
|
|
@ -521,7 +521,7 @@
|
|||
"poll.total_people": "{count, plural, one {# persona} other {# persones}}",
|
||||
"poll.total_votes": "{count, plural, one {# vot} other {# vots}}",
|
||||
"poll.vote": "Vota",
|
||||
"poll.voted": "Vas votar per aquesta resposta",
|
||||
"poll.voted": "Vau votar aquesta resposta",
|
||||
"poll.votes": "{votes, plural, one {# vot} other {# vots}}",
|
||||
"poll_button.add_poll": "Afegeix una enquesta",
|
||||
"poll_button.remove_poll": "Elimina l'enquesta",
|
||||
|
|
|
@ -96,7 +96,6 @@
|
|||
"announcement.announcement": "Announcement",
|
||||
"attachments_list.unprocessed": "(unprocessed)",
|
||||
"audio.hide": "Hide audio",
|
||||
"autosuggest_hashtag.per_week": "{count} per week",
|
||||
"boost_modal.combo": "You can press {combo} to skip this next time",
|
||||
"bundle_column_error.copy_stacktrace": "Copy error report",
|
||||
"bundle_column_error.error.body": "The requested page could not be rendered. It could be due to a bug in our code, or a browser compatibility issue.",
|
||||
|
@ -161,23 +160,23 @@
|
|||
"compose_form.markdown.unmarked": "Markdown is NOT available",
|
||||
"compose_form.mention_warning": "When you add a mention to a limited post, the person you are mentioning can also see this post.",
|
||||
"compose_form.placeholder": "What's on your mind?",
|
||||
"compose_form.searchability_warning": "Self only searchability is not available other mastodon servers. Others can search your post.",
|
||||
"compose_form.poll.add_option": "Add a choice",
|
||||
"compose_form.poll.add_option": "Add option",
|
||||
"compose_form.poll.duration": "Poll duration",
|
||||
"compose_form.poll.option_placeholder": "Choice {number}",
|
||||
"compose_form.poll.remove_option": "Remove this choice",
|
||||
"compose_form.poll.multiple": "Multiple choice",
|
||||
"compose_form.poll.option_placeholder": "Option {number}",
|
||||
"compose_form.poll.remove_option": "Remove this option",
|
||||
"compose_form.poll.single": "Pick one",
|
||||
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
|
||||
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
||||
"compose_form.publish": "Publish",
|
||||
"compose_form.poll.type": "Style",
|
||||
"compose_form.publish": "Post",
|
||||
"compose_form.publish_form": "New post",
|
||||
"compose_form.publish_loud": "{publish}!",
|
||||
"compose_form.save_changes": "Save changes",
|
||||
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}",
|
||||
"compose_form.sensitive.marked": "{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}",
|
||||
"compose_form.sensitive.unmarked": "{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}",
|
||||
"compose_form.reply": "Reply",
|
||||
"compose_form.save_changes": "Update",
|
||||
"compose_form.searchability_warning": "Self only searchability is not available other mastodon servers. Others can search your post.",
|
||||
"compose_form.spoiler.marked": "Remove content warning",
|
||||
"compose_form.spoiler.unmarked": "Add content warning",
|
||||
"compose_form.spoiler_placeholder": "Write your warning here",
|
||||
"compose_form.spoiler_placeholder": "Content warning (optional)",
|
||||
"confirmation_modal.cancel": "Cancel",
|
||||
"confirmations.block.block_and_report": "Block & Report",
|
||||
"confirmations.block.confirm": "Block",
|
||||
|
@ -428,7 +427,6 @@
|
|||
"navigation_bar.direct": "Private mentions",
|
||||
"navigation_bar.discover": "Discover",
|
||||
"navigation_bar.domain_blocks": "Blocked domains",
|
||||
"navigation_bar.edit_profile": "Edit profile",
|
||||
"navigation_bar.explore": "Explore",
|
||||
"navigation_bar.favourites": "Favorites",
|
||||
"navigation_bar.filters": "Muted words",
|
||||
|
@ -551,22 +549,23 @@
|
|||
"poll_button.add_poll": "Add a poll",
|
||||
"poll_button.remove_poll": "Remove poll",
|
||||
"privacy.change": "Change post privacy",
|
||||
"privacy.direct.long": "Visible for mentioned users only",
|
||||
"privacy.direct.short": "Mentioned people only",
|
||||
"privacy.direct.long": "Everyone mentioned in the post",
|
||||
"privacy.direct.short": "Specific people",
|
||||
"privacy.limited.short": "Limited",
|
||||
"privacy.login.long": "Visible for login users only",
|
||||
"privacy.login.short": "Login only",
|
||||
"privacy.mutual.long": "Mutual followers only",
|
||||
"privacy.mutual.short": "Mutual only",
|
||||
"privacy.personal.short": "Yourself only",
|
||||
"privacy.private.long": "Visible for followers only",
|
||||
"privacy.private.short": "Followers only",
|
||||
"privacy.public.long": "Visible for all",
|
||||
"privacy.private.long": "Only your followers",
|
||||
"privacy.private.short": "Followers",
|
||||
"privacy.public.long": "Anyone on and off Mastodon",
|
||||
"privacy.public.short": "Public",
|
||||
"privacy.public_unlisted.long": "Visible for all without GTL",
|
||||
"privacy.public_unlisted.short": "Public unlisted",
|
||||
"privacy.unlisted.long": "Visible for all, but opted-out of discovery features",
|
||||
"privacy.unlisted.short": "Unlisted",
|
||||
"privacy.unlisted.additional": "This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.",
|
||||
"privacy.unlisted.long": "Fewer algorithmic fanfares",
|
||||
"privacy.unlisted.short": "Quiet public",
|
||||
"privacy_policy.last_updated": "Last updated {date}",
|
||||
"privacy_policy.title": "Privacy Policy",
|
||||
"reaction_deck.add": "Add",
|
||||
|
@ -586,7 +585,9 @@
|
|||
"relative_time.minutes": "{number}m",
|
||||
"relative_time.seconds": "{number}s",
|
||||
"relative_time.today": "today",
|
||||
"reply_indicator.attachments": "{count, plural, one {# attachment} other {# attachments}}",
|
||||
"reply_indicator.cancel": "Cancel",
|
||||
"reply_indicator.poll": "Poll",
|
||||
"report.block": "Block",
|
||||
"report.block_explanation": "You will not see their posts. They will not be able to see your posts or follow you. They will be able to tell that they are blocked.",
|
||||
"report.categories.legal": "Legal",
|
||||
|
@ -768,10 +769,8 @@
|
|||
"upload_error.poll": "File upload not allowed with polls.",
|
||||
"upload_form.audio_description": "Describe for people who are deaf or hard of hearing",
|
||||
"upload_form.description": "Describe for people who are blind or have low vision",
|
||||
"upload_form.description_missing": "No description added",
|
||||
"upload_form.edit": "Edit",
|
||||
"upload_form.thumbnail": "Change thumbnail",
|
||||
"upload_form.undo": "Delete",
|
||||
"upload_form.video_description": "Describe for people who are deaf, hard of hearing, blind or have low vision",
|
||||
"upload_modal.analyzing_picture": "Analyzing picture…",
|
||||
"upload_modal.apply": "Apply",
|
||||
|
|
|
@ -216,7 +216,7 @@
|
|||
"compose_form.direct_message_warning_learn_more": "もっと詳しく",
|
||||
"compose_form.encryption_warning": "Mastodonの投稿はエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。",
|
||||
"compose_form.hashtag_warning": "この投稿は公開設定ではないのでハッシュタグの一覧に表示されません。公開投稿だけがハッシュタグで検索できます。",
|
||||
"compose_form.limited_post_warning": "限定投稿は現状、ごく一部のMastodonサーバーにしか届きません(2023年9月時点でFedibird、kmyblueなど一部のみです)",
|
||||
"compose_form.limited_post_warning": "限定投稿は、v4.3.0以降のMastodon、またはkmyblue・Fedibirdを含む一部のMastodonにしか届きません(2024年1月時点)",
|
||||
"compose_form.lock_disclaimer": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。",
|
||||
"compose_form.lock_disclaimer.lock": "承認制",
|
||||
"compose_form.markdown.marked": "Markdown有効",
|
||||
|
@ -634,15 +634,15 @@
|
|||
"poll_button.add_poll": "アンケートを追加",
|
||||
"poll_button.remove_poll": "アンケートを削除",
|
||||
"privacy.change": "公開範囲を変更",
|
||||
"privacy.circle.long": "サークルメンバーのみ",
|
||||
"privacy.circle.short": "サークル",
|
||||
"privacy.circle.long": "サークルメンバーのみ閲覧可",
|
||||
"privacy.circle.short": "サークル (投稿時点)",
|
||||
"privacy.direct.long": "指定された相手のみ閲覧可",
|
||||
"privacy.direct.short": "指定された相手のみ",
|
||||
"privacy.limited.short": "限定投稿",
|
||||
"privacy.login.long": "ログインユーザーのみ閲覧可、公開",
|
||||
"privacy.login.short": "ログインユーザーのみ",
|
||||
"privacy.mutual.long": "相互フォローさんのみ閲覧可、限定投稿",
|
||||
"privacy.mutual.short": "相互のみ",
|
||||
"privacy.mutual.long": "相互フォローのみ閲覧可",
|
||||
"privacy.mutual.short": "相互 (投稿時点)",
|
||||
"privacy.personal.short": "自分限定",
|
||||
"privacy.private.long": "フォロワーのみ閲覧可",
|
||||
"privacy.private.short": "フォロワーのみ",
|
||||
|
@ -650,6 +650,8 @@
|
|||
"privacy.public.short": "公開",
|
||||
"privacy.public_unlisted.long": "誰でも閲覧可、ホーム+ローカルTL",
|
||||
"privacy.public_unlisted.short": "ローカル公開",
|
||||
"privacy.reply.long": "元投稿と同じメンバーが閲覧可",
|
||||
"privacy.reply.short": "限定投稿への返信",
|
||||
"privacy.unlisted.long": "誰でも閲覧可、ホームTL",
|
||||
"privacy.unlisted.short": "非収載",
|
||||
"privacy_policy.last_updated": "{date}に更新",
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"account.blocked": "Blocat",
|
||||
"account.browse_more_on_origin_server": "Navigar sul perfil original",
|
||||
"account.cancel_follow_request": "Retirar la demanda d’abonament",
|
||||
"account.copy": "Copiar lo ligam del perfil",
|
||||
"account.direct": "Mencionar @{name} en privat",
|
||||
"account.disable_notifications": "Quitar de m’avisar quand @{name} publica quicòm",
|
||||
"account.domain_blocked": "Domeni amagat",
|
||||
|
@ -28,6 +29,7 @@
|
|||
"account.featured_tags.last_status_never": "Cap de publicacion",
|
||||
"account.featured_tags.title": "Etiquetas en avant de {name}",
|
||||
"account.follow": "Sègre",
|
||||
"account.follow_back": "Sègre en retorn",
|
||||
"account.followers": "Seguidors",
|
||||
"account.followers.empty": "Degun sèc pas aqueste utilizaire pel moment.",
|
||||
"account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidors}}",
|
||||
|
@ -48,6 +50,7 @@
|
|||
"account.mute_notifications_short": "Amudir las notificacions",
|
||||
"account.mute_short": "Amudir",
|
||||
"account.muted": "Mes en silenci",
|
||||
"account.mutual": "Mutual",
|
||||
"account.no_bio": "Cap de descripcion pas fornida.",
|
||||
"account.open_original_page": "Dobrir la pagina d’origina",
|
||||
"account.posts": "Tuts",
|
||||
|
@ -172,6 +175,7 @@
|
|||
"conversation.mark_as_read": "Marcar coma legida",
|
||||
"conversation.open": "Veire la conversacion",
|
||||
"conversation.with": "Amb {names}",
|
||||
"copy_icon_button.copied": "Copiat al quichapapièr",
|
||||
"copypaste.copied": "Copiat",
|
||||
"copypaste.copy_to_clipboard": "Copiar al quichapapièr",
|
||||
"directory.federated": "Del fediverse conegut",
|
||||
|
@ -294,6 +298,8 @@
|
|||
"keyboard_shortcuts.direct": "to open direct messages column",
|
||||
"keyboard_shortcuts.down": "far davalar dins la lista",
|
||||
"keyboard_shortcuts.enter": "dobrir los estatuts",
|
||||
"keyboard_shortcuts.favourite": "Marcar coma favorit",
|
||||
"keyboard_shortcuts.favourites": "Dobrir la lista dels favorits",
|
||||
"keyboard_shortcuts.federated": "dobrir lo flux public global",
|
||||
"keyboard_shortcuts.heading": "Acorchis clavièr",
|
||||
"keyboard_shortcuts.home": "dobrir lo flux public local",
|
||||
|
@ -339,6 +345,7 @@
|
|||
"lists.search": "Cercar demest lo mond que seguètz",
|
||||
"lists.subheading": "Vòstras listas",
|
||||
"load_pending": "{count, plural, one {# nòu element} other {# nòu elements}}",
|
||||
"loading_indicator.label": "Cargament…",
|
||||
"media_gallery.toggle_visible": "Modificar la visibilitat",
|
||||
"mute_modal.duration": "Durada",
|
||||
"mute_modal.hide_notifications": "Rescondre las notificacions d’aquesta persona ?",
|
||||
|
@ -371,6 +378,7 @@
|
|||
"not_signed_in_indicator.not_signed_in": "Devètz vos connectar per accedir a aquesta ressorsa.",
|
||||
"notification.admin.report": "{name} senhalèt {target}",
|
||||
"notification.admin.sign_up": "{name} se marquèt",
|
||||
"notification.favourite": "{name} a mes vòstre estatut en favorit",
|
||||
"notification.follow": "{name} vos sèc",
|
||||
"notification.follow_request": "{name} a demandat a vos sègre",
|
||||
"notification.mention": "{name} vos a mencionat",
|
||||
|
@ -423,6 +431,8 @@
|
|||
"onboarding.compose.template": "Adiu #Mastodon !",
|
||||
"onboarding.follows.lead": "You curate your own home feed. The more people you follow, the more active and interesting it will be. These profiles may be a good starting point—you can always unfollow them later!",
|
||||
"onboarding.follows.title": "Popular on Mastodon",
|
||||
"onboarding.profile.display_name": "Nom d’afichatge",
|
||||
"onboarding.profile.note": "Biografia",
|
||||
"onboarding.share.title": "Partejar vòstre perfil",
|
||||
"onboarding.start.lead": "Your new Mastodon account is ready to go. Here's how you can make the most of it:",
|
||||
"onboarding.start.skip": "Want to skip right ahead?",
|
||||
|
@ -504,6 +514,7 @@
|
|||
"report_notification.categories.spam": "Messatge indesirable",
|
||||
"report_notification.categories.violation": "Violacion de las règlas",
|
||||
"report_notification.open": "Dobrir lo senhalament",
|
||||
"search.no_recent_searches": "Cap de recèrcas recentas",
|
||||
"search.placeholder": "Recercar",
|
||||
"search.search_or_paste": "Recercar o picar una URL",
|
||||
"search_popout.language_code": "Còdi ISO de lenga",
|
||||
|
@ -536,6 +547,7 @@
|
|||
"status.copy": "Copiar lo ligam de l’estatut",
|
||||
"status.delete": "Escafar",
|
||||
"status.detailed_status": "Vista detalhada de la convèrsa",
|
||||
"status.direct": "Mencionar @{name} en privat",
|
||||
"status.direct_indicator": "Mencion privada",
|
||||
"status.edit": "Modificar",
|
||||
"status.edited": "Modificat {date}",
|
||||
|
@ -626,6 +638,7 @@
|
|||
"upload_modal.preview_label": "Apercebut ({ratio})",
|
||||
"upload_progress.label": "Mandadís…",
|
||||
"upload_progress.processing": "Tractament…",
|
||||
"username.taken": "Aqueste nom d’utilizaire es pres. Ensajatz-ne un autre",
|
||||
"video.close": "Tampar la vidèo",
|
||||
"video.download": "Telecargar lo fichièr",
|
||||
"video.exit_fullscreen": "Sortir plen ecran",
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
"account.locked_info": "此帳號的隱私狀態設定為鎖定。該擁有者會手動審核能跟隨此帳號的人。",
|
||||
"account.media": "媒體",
|
||||
"account.mention": "提及 @{name}",
|
||||
"account.moved_to": "{name} 現在的新帳號為:",
|
||||
"account.moved_to": "{name} 目前的新帳號為:",
|
||||
"account.mute": "靜音 @{name}",
|
||||
"account.mute_notifications_short": "靜音推播通知",
|
||||
"account.mute_short": "靜音",
|
||||
|
@ -59,7 +59,7 @@
|
|||
"account.posts": "嘟文",
|
||||
"account.posts_with_replies": "嘟文與回覆",
|
||||
"account.report": "檢舉 @{name}",
|
||||
"account.requested": "正在等待核准。按一下以取消跟隨請求",
|
||||
"account.requested": "正在等候審核。按一下以取消跟隨請求",
|
||||
"account.requested_follow": "{name} 要求跟隨您",
|
||||
"account.share": "分享 @{name} 的個人檔案",
|
||||
"account.show_reblogs": "顯示來自 @{name} 的嘟文",
|
||||
|
@ -84,7 +84,7 @@
|
|||
"admin.impact_report.title": "影響總結",
|
||||
"alert.rate_limited.message": "請於 {retry_time, time, medium} 後重試。",
|
||||
"alert.rate_limited.title": "已限速",
|
||||
"alert.unexpected.message": "發生了非預期的錯誤。",
|
||||
"alert.unexpected.message": "發生非預期的錯誤。",
|
||||
"alert.unexpected.title": "哎呀!",
|
||||
"announcement.announcement": "公告",
|
||||
"attachments_list.unprocessed": "(未經處理)",
|
||||
|
@ -241,7 +241,7 @@
|
|||
"empty_column.followed_tags": "您還沒有跟隨任何主題標籤。當您跟隨主題標籤時,它們將於此顯示。",
|
||||
"empty_column.hashtag": "這個主題標籤下什麼也沒有。",
|
||||
"empty_column.home": "您的首頁時間軸是空的!跟隨更多人來將它填滿吧!",
|
||||
"empty_column.list": "這份列表下什麼也沒有。當此列表的成員嘟出了新的嘟文時,它們將顯示於此。",
|
||||
"empty_column.list": "這份列表下什麼也沒有。當此列表的成員嘟出新的嘟文時,它們將顯示於此。",
|
||||
"empty_column.lists": "您還沒有建立任何列表。當您建立列表時,它將於此顯示。",
|
||||
"empty_column.mutes": "您尚未靜音任何使用者。",
|
||||
"empty_column.notifications": "您還沒有收到任何通知,當您與別人開始互動時,它將於此顯示。",
|
||||
|
@ -303,8 +303,8 @@
|
|||
"hashtag.counter_by_accounts": "{count, plural, one {{counter} 名} other {{counter} 名}}參與者",
|
||||
"hashtag.counter_by_uses": "{count, plural, one {{counter} 則} other {{counter} 則}}嘟文",
|
||||
"hashtag.counter_by_uses_today": "本日有 {count, plural, one {{counter} 則} other {{counter} 則}}嘟文",
|
||||
"hashtag.follow": "追蹤主題標籤",
|
||||
"hashtag.unfollow": "取消追蹤主題標籤",
|
||||
"hashtag.follow": "跟隨主題標籤",
|
||||
"hashtag.unfollow": "取消跟隨主題標籤",
|
||||
"hashtags.and_other": "…及其他 {count, plural, other {# 個}}",
|
||||
"home.actions.go_to_explore": "看看發生什麼新鮮事",
|
||||
"home.actions.go_to_suggestions": "尋找一些人來跟隨",
|
||||
|
|
|
@ -43,9 +43,7 @@ import {
|
|||
COMPOSE_RESET,
|
||||
COMPOSE_POLL_ADD,
|
||||
COMPOSE_POLL_REMOVE,
|
||||
COMPOSE_POLL_OPTION_ADD,
|
||||
COMPOSE_POLL_OPTION_CHANGE,
|
||||
COMPOSE_POLL_OPTION_REMOVE,
|
||||
COMPOSE_POLL_SETTINGS_CHANGE,
|
||||
COMPOSE_CIRCLE_CHANGE,
|
||||
INIT_MEDIA_EDIT_MODAL,
|
||||
|
@ -365,6 +363,18 @@ const updateSuggestionTags = (state, token) => {
|
|||
});
|
||||
};
|
||||
|
||||
const updatePoll = (state, index, value) => state.updateIn(['poll', 'options'], options => {
|
||||
const tmp = options.set(index, value).filterNot(x => x.trim().length === 0);
|
||||
|
||||
if (tmp.size === 0) {
|
||||
return tmp.push('').push('');
|
||||
} else if (tmp.size < 8) {
|
||||
return tmp.push('');
|
||||
}
|
||||
|
||||
return tmp;
|
||||
});
|
||||
|
||||
export default function compose(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case STORE_HYDRATE:
|
||||
|
@ -635,12 +645,8 @@ export default function compose(state = initialState, action) {
|
|||
return state.set('poll', initialPoll);
|
||||
case COMPOSE_POLL_REMOVE:
|
||||
return state.set('poll', null);
|
||||
case COMPOSE_POLL_OPTION_ADD:
|
||||
return state.updateIn(['poll', 'options'], options => options.push(action.title));
|
||||
case COMPOSE_POLL_OPTION_CHANGE:
|
||||
return state.setIn(['poll', 'options', action.index], action.title);
|
||||
case COMPOSE_POLL_OPTION_REMOVE:
|
||||
return state.updateIn(['poll', 'options'], options => options.delete(action.index));
|
||||
return updatePoll(state, action.index, action.title);
|
||||
case COMPOSE_POLL_SETTINGS_CHANGE:
|
||||
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
|
||||
case COMPOSE_CIRCLE_CHANGE:
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M80-120v-80h800v80H80Zm40-120v-280h120v280H120Zm200 0v-480h120v480H320Zm200 0v-360h120v360H520Zm200 0v-600h120v600H720Z"/></svg>
|
After Width: | Height: | Size: 225 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M80-120v-80h800v80H80Zm40-120v-280h120v280H120Zm200 0v-480h120v480H320Zm200 0v-360h120v360H520Zm200 0v-600h120v600H720Z"/></svg>
|
After Width: | Height: | Size: 225 B |
1
app/javascript/material-icons/400-24px/markdown-fill.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m640-360 120-120-42-43-48 48v-125h-60v125l-48-48-42 43 120 120ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm60-200h60v-180h40v120h60v-120h40v180h60v-200q0-17-11.5-28.5T440-600H260q-17 0-28.5 11.5T220-560v200Z"/></svg>
|
After Width: | Height: | Size: 388 B |
1
app/javascript/material-icons/400-24px/markdown.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m640-360 120-120-42-43-48 48v-125h-60v125l-48-48-42 43 120 120ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-480H160v480Zm0 0v-480 480Zm60-120h60v-180h40v120h60v-120h40v180h60v-200q0-17-11.5-28.5T440-600H260q-17 0-28.5 11.5T220-560v200Z"/></svg>
|
After Width: | Height: | Size: 425 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M360-400h400L622-580l-92 120-62-80-108 140Zm-40 160q-33 0-56.5-23.5T240-320v-480q0-33 23.5-56.5T320-880h480q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H320ZM160-80q-33 0-56.5-23.5T80-160v-560h80v560h560v80H160Z"/></svg>
|
After Width: | Height: | Size: 322 B |
1
app/javascript/material-icons/400-24px/photo_library.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M360-400h400L622-580l-92 120-62-80-108 140Zm-40 160q-33 0-56.5-23.5T240-320v-480q0-33 23.5-56.5T320-880h480q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H320Zm0-80h480v-480H320v480ZM160-80q-33 0-56.5-23.5T80-160v-560h80v560h560v80H160Zm160-720v480-480Z"/></svg>
|
After Width: | Height: | Size: 362 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M524-40q-84 0-157.5-32t-128-86.5Q184-213 152-286.5T120-444q0-146 93-257.5T450-840q-18 99 11 193.5T561-481q71 71 165.5 100T920-370q-26 144-138 237T524-40Z"/></svg>
|
After Width: | Height: | Size: 259 B |
1
app/javascript/material-icons/400-24px/quiet_time.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M524-40q-84 0-157.5-32t-128-86.5Q184-213 152-286.5T120-444q0-146 93-257.5T450-840q-18 99 11 193.5T561-481q71 71 165.5 100T920-370q-26 144-138 237T524-40Zm0-80q88 0 163-44t118-121q-86-8-163-43.5T504-425q-61-61-97-138t-43-163q-77 43-120.5 118.5T200-444q0 135 94.5 229.5T524-120Zm-20-305Z"/></svg>
|
After Width: | Height: | Size: 391 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m476-80 182-480h84L924-80h-84l-43-122H603L560-80h-84ZM160-200l-56-56 202-202q-35-35-63.5-80T190-640h84q20 39 40 68t48 58q33-33 68.5-92.5T484-720H40v-80h280v-80h80v80h280v80H564q-21 72-63 148t-83 116l96 98-30 82-122-125-202 201Zm468-72h144l-72-204-72 204Z"/></svg>
|
After Width: | Height: | Size: 360 B |
1
app/javascript/material-icons/400-24px/translate.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m476-80 182-480h84L924-80h-84l-43-122H603L560-80h-84ZM160-200l-56-56 202-202q-35-35-63.5-80T190-640h84q20 39 40 68t48 58q33-33 68.5-92.5T484-720H40v-80h280v-80h80v80h280v80H564q-21 72-63 148t-83 116l96 98-30 82-122-125-202 201Zm468-72h144l-72-204-72 204Z"/></svg>
|
After Width: | Height: | Size: 360 B |
1
app/javascript/material-icons/400-24px/warning-fill.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m40-120 440-760 440 760H40Zm440-120q17 0 28.5-11.5T520-280q0-17-11.5-28.5T480-320q-17 0-28.5 11.5T440-280q0 17 11.5 28.5T480-240Zm-40-120h80v-200h-80v200Z"/></svg>
|
After Width: | Height: | Size: 260 B |
1
app/javascript/material-icons/400-24px/warning.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="m40-120 440-760 440 760H40Zm138-80h604L480-720 178-200Zm302-40q17 0 28.5-11.5T520-280q0-17-11.5-28.5T480-320q-17 0-28.5 11.5T440-280q0 17 11.5 28.5T480-240Zm-40-120h80v-200h-80v200Zm40-100Z"/></svg>
|
After Width: | Height: | Size: 295 B |
|
@ -13,10 +13,12 @@ function loaded() {
|
|||
|
||||
if (mountNode) {
|
||||
const attr = mountNode.getAttribute('data-props');
|
||||
if(!attr) return;
|
||||
|
||||
if (!attr) return;
|
||||
|
||||
const props = JSON.parse(attr);
|
||||
const root = createRoot(mountNode);
|
||||
|
||||
root.render(<ComposeContainer {...props} />);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,7 @@
|
|||
.compose-form {
|
||||
.compose-form__modifiers {
|
||||
.compose-form__upload {
|
||||
&-description {
|
||||
input {
|
||||
&::placeholder {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.status__content a,
|
||||
.link-footer a,
|
||||
.reply-indicator__content a,
|
||||
.edit-indicator__content a,
|
||||
.link-footer a,
|
||||
.status__content__read-more-button,
|
||||
.status__content__translate-button {
|
||||
text-decoration: underline;
|
||||
|
@ -42,7 +29,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.status__content a {
|
||||
.status__content a,
|
||||
.reply-indicator__content a,
|
||||
.edit-indicator__content a {
|
||||
color: $highlight-text-color;
|
||||
}
|
||||
|
||||
|
@ -50,24 +39,10 @@
|
|||
color: $darker-text-color;
|
||||
}
|
||||
|
||||
.compose-form__poll-wrapper .button.button-secondary,
|
||||
.compose-form .autosuggest-textarea__textarea::placeholder,
|
||||
.compose-form .spoiler-input__input::placeholder,
|
||||
.report-dialog-modal__textarea::placeholder,
|
||||
.language-dropdown__dropdown__results__item__common-name,
|
||||
.compose-form .icon-button {
|
||||
.report-dialog-modal__textarea::placeholder {
|
||||
color: $inverted-text-color;
|
||||
}
|
||||
|
||||
.text-icon-button.active {
|
||||
color: $ui-highlight-color;
|
||||
}
|
||||
|
||||
.language-dropdown__dropdown__results__item.active {
|
||||
background: $ui-highlight-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.link-button:disabled {
|
||||
cursor: not-allowed;
|
||||
|
||||
|
|
|
@ -33,9 +33,6 @@ textarea {
|
|||
}
|
||||
|
||||
.compose-form .compose-form__warning,
|
||||
.reply-indicator__content,
|
||||
.reply-indicator__display-name,
|
||||
.reply-indicator__cancel,
|
||||
.autosuggest-textarea__suggestions__item {
|
||||
color: $ui-base-color;
|
||||
}
|
||||
|
|
|
@ -145,10 +145,6 @@ html {
|
|||
}
|
||||
}
|
||||
|
||||
.compose-form__autosuggest-wrapper,
|
||||
.poll__option input[type='text'],
|
||||
.compose-form .spoiler-input__input,
|
||||
.compose-form__poll-wrapper select,
|
||||
.search__input,
|
||||
.setting-text,
|
||||
.report-dialog-modal__textarea,
|
||||
|
@ -172,28 +168,11 @@ html {
|
|||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.compose-form__poll-wrapper select {
|
||||
background: $simple-background-color
|
||||
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>")
|
||||
no-repeat right 8px center / auto 16px;
|
||||
}
|
||||
|
||||
.compose-form__poll-wrapper,
|
||||
.compose-form__poll-wrapper .poll__footer {
|
||||
border-top-color: lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
.notification__filter-bar {
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.compose-form .compose-form__buttons-wrapper {
|
||||
background: $ui-base-color;
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.drawer__header,
|
||||
.drawer__inner {
|
||||
background: $white;
|
||||
|
@ -206,52 +185,6 @@ html {
|
|||
no-repeat bottom / 100% auto;
|
||||
}
|
||||
|
||||
// Change the colors used in compose-form
|
||||
.compose-form {
|
||||
.compose-form__modifiers {
|
||||
.compose-form__upload__actions .icon-button,
|
||||
.compose-form__upload__warning .icon-button {
|
||||
color: lighten($white, 7%);
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.compose-form__buttons-wrapper {
|
||||
background: darken($ui-base-color, 6%);
|
||||
}
|
||||
|
||||
.autosuggest-textarea__suggestions {
|
||||
background: darken($ui-base-color, 6%);
|
||||
}
|
||||
|
||||
.autosuggest-textarea__suggestions__item {
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active,
|
||||
&.selected {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-mart-bar {
|
||||
border-color: lighten($ui-base-color, 4%);
|
||||
|
||||
&:first-child {
|
||||
background: darken($ui-base-color, 6%);
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-mart-search input {
|
||||
background: rgba($ui-base-color, 0.3);
|
||||
border-color: $ui-base-color;
|
||||
}
|
||||
|
||||
.upload-progress__backdrop {
|
||||
background: $ui-base-color;
|
||||
}
|
||||
|
@ -283,55 +216,11 @@ html {
|
|||
background: $ui-base-color;
|
||||
}
|
||||
|
||||
.privacy-dropdown.active .privacy-dropdown__value.active .icon-button,
|
||||
.expiration-dropdown.active .expiration-dropdown__value.active .icon-button {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.account-gallery__item a {
|
||||
background-color: $ui-base-color;
|
||||
}
|
||||
|
||||
// Change the colors used in the dropdown menu
|
||||
.dropdown-menu {
|
||||
background: $white;
|
||||
|
||||
&__arrow::before {
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
&__item {
|
||||
color: $darker-text-color;
|
||||
|
||||
&--dangerous {
|
||||
color: $error-value-color;
|
||||
}
|
||||
|
||||
a,
|
||||
button {
|
||||
background: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Change the text colors on inverted background
|
||||
.privacy-dropdown__option.active,
|
||||
.privacy-dropdown__option:hover,
|
||||
.privacy-dropdown__option.active .privacy-dropdown__option__content,
|
||||
.privacy-dropdown__option.active .privacy-dropdown__option__content strong,
|
||||
.privacy-dropdown__option:hover .privacy-dropdown__option__content,
|
||||
.privacy-dropdown__option:hover .privacy-dropdown__option__content strong,
|
||||
.expiration-dropdown__option.active,
|
||||
.expiration-dropdown__option:hover,
|
||||
.expiration-dropdown__option.active .expiration-dropdown__option__content,
|
||||
.expiration-dropdown__option.active
|
||||
.expiration-dropdown__option__content
|
||||
strong,
|
||||
.expiration-dropdown__option:hover .expiration-dropdown__option__content,
|
||||
.expiration-dropdown__option:hover .expiration-dropdown__option__content strong,
|
||||
.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:active,
|
||||
.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:focus,
|
||||
.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:hover,
|
||||
.actions-modal ul li:not(:empty) a.active,
|
||||
.actions-modal ul li:not(:empty) a.active button,
|
||||
.actions-modal ul li:not(:empty) a:active,
|
||||
|
@ -340,7 +229,6 @@ html {
|
|||
.actions-modal ul li:not(:empty) a:focus button,
|
||||
.actions-modal ul li:not(:empty) a:hover,
|
||||
.actions-modal ul li:not(:empty) a:hover button,
|
||||
.language-dropdown__dropdown__results__item.active,
|
||||
.admin-wrapper .sidebar ul .simple-navigation-active-leaf a,
|
||||
.simple_form .block-button,
|
||||
.simple_form .button,
|
||||
|
@ -348,19 +236,6 @@ html {
|
|||
color: $white;
|
||||
}
|
||||
|
||||
.language-dropdown__dropdown__results__item
|
||||
.language-dropdown__dropdown__results__item__common-name {
|
||||
color: lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
.language-dropdown__dropdown__results__item.active
|
||||
.language-dropdown__dropdown__results__item__common-name {
|
||||
color: darken($ui-base-color, 12%);
|
||||
}
|
||||
|
||||
.dropdown-menu__separator,
|
||||
.dropdown-menu__item.edited-timestamp__history__item,
|
||||
.dropdown-menu__container__header,
|
||||
.compare-history-modal .report-modal__target,
|
||||
.report-dialog-modal .poll__option.dialog-option {
|
||||
border-bottom-color: lighten($ui-base-color, 4%);
|
||||
|
@ -394,10 +269,7 @@ html {
|
|||
|
||||
.reactions-bar__item:hover,
|
||||
.reactions-bar__item:focus,
|
||||
.reactions-bar__item:active,
|
||||
.language-dropdown__dropdown__results__item:hover,
|
||||
.language-dropdown__dropdown__results__item:focus,
|
||||
.language-dropdown__dropdown__results__item:active {
|
||||
.reactions-bar__item:active {
|
||||
background-color: $ui-base-color;
|
||||
}
|
||||
|
||||
|
@ -640,11 +512,6 @@ html {
|
|||
}
|
||||
}
|
||||
|
||||
.reply-indicator {
|
||||
background: transparent;
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
.status__content,
|
||||
.reply-indicator__content {
|
||||
a {
|
||||
|
@ -684,3 +551,30 @@ html {
|
|||
background-color: rgba($ui-highlight-color, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.compose-form__actions .icon-button.active,
|
||||
.dropdown-button.active,
|
||||
.privacy-dropdown__option.active,
|
||||
.privacy-dropdown__option:focus,
|
||||
.language-dropdown__dropdown__results__item:focus,
|
||||
.language-dropdown__dropdown__results__item.active,
|
||||
.privacy-dropdown__option:focus .privacy-dropdown__option__content,
|
||||
.privacy-dropdown__option:focus .privacy-dropdown__option__content strong,
|
||||
.privacy-dropdown__option.active .privacy-dropdown__option__content,
|
||||
.privacy-dropdown__option.active .privacy-dropdown__option__content strong,
|
||||
.language-dropdown__dropdown__results__item:focus
|
||||
.language-dropdown__dropdown__results__item__common-name,
|
||||
.language-dropdown__dropdown__results__item.active
|
||||
.language-dropdown__dropdown__results__item__common-name {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.compose-form .spoiler-input__input {
|
||||
color: lighten($ui-highlight-color, 8%);
|
||||
}
|
||||
|
||||
.compose-form .autosuggest-textarea__textarea,
|
||||
.compose-form__highlightable,
|
||||
.poll__option input[type='text'] {
|
||||
background: darken($ui-base-color, 10%);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ $white: #ffffff;
|
|||
$classic-base-color: #282c37;
|
||||
$classic-primary-color: #9baec8;
|
||||
$classic-secondary-color: #d9e1e8;
|
||||
$classic-highlight-color: #858afa;
|
||||
$classic-highlight-color: #6364ff;
|
||||
|
||||
$blurple-600: #563acc; // Iris
|
||||
$blurple-500: #6364ff; // Brand purple
|
||||
|
@ -37,7 +37,7 @@ $ui-button-tertiary-border-color: $blurple-500 !default;
|
|||
|
||||
$primary-text-color: $black !default;
|
||||
$darker-text-color: $classic-base-color !default;
|
||||
$highlight-text-color: darken($ui-highlight-color, 8%) !default;
|
||||
$highlight-text-color: $ui-highlight-color !default;
|
||||
$dark-text-color: #444b5d;
|
||||
$action-button-color: #606984;
|
||||
|
||||
|
@ -58,3 +58,8 @@ $account-background-color: $white !default;
|
|||
}
|
||||
|
||||
$emojis-requiring-inversion: 'chains';
|
||||
|
||||
.theme-mastodon-light {
|
||||
--dropdown-border-color: #d9e1e8;
|
||||
--dropdown-background-color: #fff;
|
||||
}
|
||||
|
|
|
@ -15,13 +15,14 @@
|
|||
outline: 0;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
font-family: inherit;
|
||||
background: $ui-base-color;
|
||||
color: $darker-text-color;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
font-size: 17px;
|
||||
line-height: normal;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
|
|
@ -1321,6 +1321,9 @@ a.sparkline {
|
|||
|
||||
&__label {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__rules {
|
||||
|
@ -1331,6 +1334,9 @@ a.sparkline {
|
|||
&__rule {
|
||||
cursor: pointer;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
body {
|
||||
font-family: $font-sans-serif, sans-serif;
|
||||
background: darken($ui-base-color, 7%);
|
||||
background: darken($ui-base-color, 8%);
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
font-weight: 400;
|
||||
|
|
|
@ -40,13 +40,12 @@
|
|||
.compose-form {
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px 0;
|
||||
margin-top: 40px;
|
||||
padding: 10px 0;
|
||||
padding-bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
|
||||
@media screen and (width <= 400px) {
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
@ -56,13 +55,15 @@
|
|||
width: 400px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
box-sizing: border-box;
|
||||
padding: 20px 0;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid $ui-base-color;
|
||||
border-bottom: 1px solid lighten($ui-base-color, 8%);
|
||||
|
||||
@media screen and (width <= 440px) {
|
||||
width: 100%;
|
||||
|
@ -71,9 +72,9 @@
|
|||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-inline-end: 10px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
|
@ -87,13 +88,14 @@
|
|||
.name {
|
||||
flex: 1 1 auto;
|
||||
color: $secondary-text-color;
|
||||
width: calc(100% - 90px);
|
||||
|
||||
.username {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
color: $primary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,7 +103,7 @@
|
|||
display: block;
|
||||
font-size: 32px;
|
||||
line-height: 40px;
|
||||
margin-inline-start: 10px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
.emoji-mart {
|
||||
font-size: 13px;
|
||||
display: inline-block;
|
||||
color: $inverted-text-color;
|
||||
|
||||
&,
|
||||
* {
|
||||
|
@ -15,13 +14,13 @@
|
|||
}
|
||||
|
||||
.emoji-mart-bar {
|
||||
border: 0 solid darken($ui-secondary-color, 8%);
|
||||
border: 0 solid var(--dropdown-border-color);
|
||||
|
||||
&:first-child {
|
||||
border-bottom-width: 1px;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
background: $ui-secondary-color;
|
||||
background: var(--dropdown-border-color);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
|
@ -36,7 +35,6 @@
|
|||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 6px;
|
||||
color: $lighter-text-color;
|
||||
line-height: 0;
|
||||
}
|
||||
|
||||
|
@ -50,9 +48,10 @@
|
|||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: $darker-text-color;
|
||||
|
||||
&:hover {
|
||||
color: darken($lighter-text-color, 4%);
|
||||
color: lighten($darker-text-color, 4%);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +59,7 @@
|
|||
color: $highlight-text-color;
|
||||
|
||||
&:hover {
|
||||
color: darken($highlight-text-color, 4%);
|
||||
color: lighten($highlight-text-color, 4%);
|
||||
}
|
||||
|
||||
.emoji-mart-anchor-bar {
|
||||
|
@ -95,7 +94,7 @@
|
|||
height: 270px;
|
||||
max-height: 35vh;
|
||||
padding: 0 6px 6px;
|
||||
background: $simple-background-color;
|
||||
background: var(--dropdown-background-color);
|
||||
will-change: transform;
|
||||
|
||||
&::-webkit-scrollbar-track:hover,
|
||||
|
@ -107,7 +106,7 @@
|
|||
.emoji-mart-search {
|
||||
padding: 10px;
|
||||
padding-inline-end: 45px;
|
||||
background: $simple-background-color;
|
||||
background: var(--dropdown-background-color);
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
|
@ -118,9 +117,9 @@
|
|||
font-family: inherit;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: rgba($ui-secondary-color, 0.3);
|
||||
color: $inverted-text-color;
|
||||
border: 1px solid $ui-secondary-color;
|
||||
background: $ui-base-color;
|
||||
color: $darker-text-color;
|
||||
border: 1px solid lighten($ui-base-color, 8%);
|
||||
border-radius: 4px;
|
||||
|
||||
&::-moz-focus-inner {
|
||||
|
@ -155,11 +154,10 @@
|
|||
&:disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: $action-button-color;
|
||||
fill: $darker-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -185,7 +183,7 @@
|
|||
inset-inline-start: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba($ui-secondary-color, 0.7);
|
||||
background-color: var(--dropdown-border-color);
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -202,7 +200,7 @@
|
|||
width: 100%;
|
||||
font-weight: 500;
|
||||
padding: 5px 6px;
|
||||
background: $simple-background-color;
|
||||
background: var(--dropdown-background-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -246,7 +244,7 @@
|
|||
|
||||
.emoji-mart-no-results {
|
||||
font-size: 14px;
|
||||
color: $light-text-color;
|
||||
color: $dark-text-color;
|
||||
text-align: center;
|
||||
padding: 5px 6px;
|
||||
padding-top: 70px;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.modal-layout {
|
||||
background: $ui-base-color
|
||||
background: darken($ui-base-color, 4%)
|
||||
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-lighter-color)}33"/></svg>')
|
||||
repeat-x bottom fixed;
|
||||
display: flex;
|
||||
|
|
|
@ -52,6 +52,8 @@
|
|||
&__option {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 6px 0;
|
||||
line-height: 18px;
|
||||
cursor: default;
|
||||
|
@ -78,16 +80,22 @@
|
|||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
color: $inverted-text-color;
|
||||
color: $secondary-text-color;
|
||||
outline: 0;
|
||||
font-family: inherit;
|
||||
background: $simple-background-color;
|
||||
border: 1px solid darken($simple-background-color, 14%);
|
||||
background: $ui-base-color;
|
||||
border: 1px solid $darker-text-color;
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
padding: 8px 12px;
|
||||
|
||||
&:focus {
|
||||
border-color: $highlight-text-color;
|
||||
border-color: $ui-highlight-color;
|
||||
}
|
||||
|
||||
@media screen and (width <= 600px) {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,26 +104,20 @@
|
|||
}
|
||||
|
||||
&.editable {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
display: inline-block;
|
||||
display: block;
|
||||
position: relative;
|
||||
border: 1px solid $ui-primary-color;
|
||||
box-sizing: border-box;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-inline-end: 10px;
|
||||
top: -1px;
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
flex: 0 0 18px;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&.checkbox {
|
||||
border-radius: 4px;
|
||||
|
@ -159,6 +161,15 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__option.editable &__input {
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: $ui-primary-color;
|
||||
border-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
&__number {
|
||||
display: inline-block;
|
||||
width: 45px;
|
||||
|
@ -209,90 +220,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.compose-form__poll-wrapper {
|
||||
border-top: 1px solid darken($simple-background-color, 8%);
|
||||
|
||||
ul {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.poll__input {
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: $ui-button-focus-background-color;
|
||||
}
|
||||
}
|
||||
|
||||
.poll__footer {
|
||||
border-top: 1px solid darken($simple-background-color, 8%);
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
button,
|
||||
select {
|
||||
flex: 1 1 50%;
|
||||
|
||||
&:focus {
|
||||
border-color: $highlight-text-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button.button-secondary {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
padding: 6px 10px;
|
||||
height: auto;
|
||||
line-height: inherit;
|
||||
color: $action-button-color;
|
||||
border-color: $action-button-color;
|
||||
margin-inline-end: 5px;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.active {
|
||||
border-color: $action-button-color;
|
||||
background-color: $action-button-color;
|
||||
color: $ui-button-color;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.poll__option {
|
||||
flex: 0 0 auto;
|
||||
width: calc(100% - (23px + 6px));
|
||||
margin-inline-end: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
appearance: none;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
color: $inverted-text-color;
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
outline: 0;
|
||||
font-family: inherit;
|
||||
background: $simple-background-color
|
||||
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(darken($simple-background-color, 14%))}'/></svg>")
|
||||
no-repeat right 8px center / auto 16px;
|
||||
border: 1px solid darken($simple-background-color, 14%);
|
||||
border-radius: 4px;
|
||||
padding: 6px 10px;
|
||||
padding-inline-end: 30px;
|
||||
}
|
||||
|
||||
.icon-button.disabled {
|
||||
color: darken($simple-background-color, 14%);
|
||||
}
|
||||
}
|
||||
|
||||
.muted .poll {
|
||||
color: $dark-text-color;
|
||||
|
||||
|
|