Merge remote-tracking branch 'parent/main' into upstream-20240126

This commit is contained in:
KMY 2024-01-26 11:14:21 +09:00
commit 3dd9591a4b
163 changed files with 3000 additions and 2777 deletions

View file

@ -165,7 +165,7 @@ module.exports = defineConfig({
// }, // },
// ], // ],
'jsx-a11y/no-noninteractive-tabindex': 'off', 'jsx-a11y/no-noninteractive-tabindex': 'off',
'jsx-a11y/no-onchange': 'warn', 'jsx-a11y/no-onchange': 'off',
// recommended is full 'error' // recommended is full 'error'
'jsx-a11y/no-static-element-interactions': [ 'jsx-a11y/no-static-element-interactions': [
'warn', 'warn',

View file

@ -78,23 +78,8 @@ jobs:
- name: Create database - name: Create database
run: './bin/rails db:create' run: './bin/rails db:create'
- name: Run migrations up to v2.0.0 - name: Run historical migrations with data population
run: './bin/rails db:migrate VERSION=20171010025614' run: './bin/rails tests:migrations:prepare_database'
- 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 all remaining migrations - name: Run all remaining migrations
run: './bin/rails db:migrate' run: './bin/rails db:migrate'

View file

@ -45,6 +45,7 @@ jobs:
--health-retries 5 --health-retries 5
ports: ports:
- 5432:5432 - 5432:5432
redis: redis:
image: redis:7-alpine image: redis:7-alpine
options: >- options: >-
@ -77,28 +78,11 @@ jobs:
- name: Create database - name: Create database
run: './bin/rails db:create' run: './bin/rails db:create'
- name: Run migrations up to v2.0.0 - name: Run historical migrations with data population
run: './bin/rails db:migrate VERSION=20171010025614' run: './bin/rails tests:migrations:prepare_database'
- 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'
env: env:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true 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 - name: Run all remaining pre-deployment migrations
run: './bin/rails db:migrate' run: './bin/rails db:migrate'
env: env:

View file

@ -1,6 +1,6 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit --no-offense-counts --no-auto-gen-timestamp` # `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 # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
@ -73,21 +73,6 @@ Rails/UniqueValidationWithoutIndex:
- 'app/models/identity.rb' - 'app/models/identity.rb'
- 'app/models/webauthn_credential.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). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowedMethods, AllowedPatterns. # Configuration parameters: AllowedMethods, AllowedPatterns.
# AllowedMethods: ==, equal?, eql? # AllowedMethods: ==, equal?, eql?
@ -139,10 +124,6 @@ Style/GlobalStdStream:
# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. # Configuration parameters: MinBodyLength, AllowConsecutiveConditionals.
Style/GuardClause: Style/GuardClause:
Exclude: 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/activitypub/activity/block.rb'
- 'app/lib/request.rb' - 'app/lib/request.rb'
- 'app/lib/request_pool.rb' - 'app/lib/request_pool.rb'
@ -298,13 +279,6 @@ Style/StringLiterals:
- 'config/routes.rb' - 'config/routes.rb'
- 'db/schema.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). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyleForMultiline. # Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma # SupportedStylesForMultiline: comma, consistent_comma, no_comma

View file

@ -123,7 +123,7 @@ group :test do
gem 'database_cleaner-active_record' gem 'database_cleaner-active_record'
# Used to mock environment variables # Used to mock environment variables
gem 'climate_control', '~> 0.2' gem 'climate_control'
# Generating fake data for specs # Generating fake data for specs
gem 'faker', '~> 3.2' gem 'faker', '~> 3.2'

View file

@ -185,7 +185,7 @@ GEM
elasticsearch (>= 7.12.0, < 7.14.0) elasticsearch (>= 7.12.0, < 7.14.0)
elasticsearch-dsl elasticsearch-dsl
chunky_png (1.4.0) chunky_png (1.4.0)
climate_control (0.2.0) climate_control (1.2.0)
cocoon (1.2.15) cocoon (1.2.15)
color_diff (0.1) color_diff (0.1)
concurrent-ruby (1.2.3) concurrent-ruby (1.2.3)
@ -360,7 +360,7 @@ GEM
rainbow (>= 2.2.2, < 4.0) rainbow (>= 2.2.2, < 4.0)
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
idn-ruby (0.1.5) idn-ruby (0.1.5)
io-console (0.7.1) io-console (0.7.2)
irb (1.11.1) irb (1.11.1)
rdoc rdoc
reline (>= 0.4.2) reline (>= 0.4.2)
@ -445,7 +445,7 @@ GEM
mime-types-data (3.2023.1205) mime-types-data (3.2023.1205)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.5) mini_portile2 (2.8.5)
minitest (5.21.1) minitest (5.21.2)
msgpack (1.7.2) msgpack (1.7.2)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.3.0) multipart-post (2.3.0)
@ -636,7 +636,7 @@ GEM
rspec-mocks (3.12.6) rspec-mocks (3.12.6)
diff-lcs (>= 1.2.0, < 2.0) diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0) rspec-support (~> 3.12.0)
rspec-rails (6.1.0) rspec-rails (6.1.1)
actionpack (>= 6.1) actionpack (>= 6.1)
activesupport (>= 6.1) activesupport (>= 6.1)
railties (>= 6.1) railties (>= 6.1)
@ -748,8 +748,8 @@ GEM
temple (0.10.3) temple (0.10.3)
terminal-table (3.0.2) terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
terrapin (0.6.0) terrapin (1.0.1)
climate_control (>= 0.0.3, < 1.0) climate_control
test-prof (1.3.1) test-prof (1.3.1)
thor (1.3.0) thor (1.3.0)
tilt (2.3.0) tilt (2.3.0)
@ -838,7 +838,7 @@ DEPENDENCIES
capybara (~> 3.39) capybara (~> 3.39)
charlock_holmes (~> 0.7.7) charlock_holmes (~> 0.7.7)
chewy (~> 7.3) chewy (~> 7.3)
climate_control (~> 0.2) climate_control
cocoon (~> 1.2) cocoon (~> 1.2)
color_diff (~> 0.1) color_diff (~> 0.1)
concurrent-ruby concurrent-ruby

View file

@ -24,7 +24,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController
def unknown_affected_account? def unknown_affected_account?
json = Oj.load(body, mode: :strict) 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 rescue Oj::ParseError
false false
end end

View file

@ -3,7 +3,7 @@
module Admin module Admin
class ConfirmationsController < BaseController class ConfirmationsController < BaseController
before_action :set_user before_action :set_user
before_action :check_confirmation, only: [:resend] before_action :redirect_confirmed_user, only: [:resend], if: :user_confirmed?
def create def create
authorize @user, :confirm? authorize @user, :confirm?
@ -25,11 +25,13 @@ module Admin
private private
def check_confirmation def redirect_confirmed_user
if @user.confirmed?
flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed') flash[:error] = I18n.t('admin.accounts.resend_confirmation.already_confirmed')
redirect_to admin_accounts_path redirect_to admin_accounts_path
end end
def user_confirmed?
@user.confirmed?
end end
end end
end end

View file

@ -38,7 +38,7 @@ module Admin
log_action :create, @email_domain_block log_action :create, @email_domain_block
(@email_domain_block.other_domains || []).uniq.each do |domain| (@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) 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 log_action :create, other_email_domain_block

View file

@ -49,7 +49,7 @@ module Admin
next next
end 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 rescue ActionController::ParameterMissing
flash.now[:alert] = I18n.t('admin.export_domain_blocks.no_file') flash.now[:alert] = I18n.t('admin.export_domain_blocks.no_file')
set_dummy_import! set_dummy_import!
@ -58,6 +58,10 @@ module Admin
private private
def instances_from_imported_blocks
Instance.with_domain_follows(@domain_blocks.map(&:domain))
end
def export_filename def export_filename
'domain_blocks.csv' 'domain_blocks.csv'
end end

View file

@ -7,7 +7,7 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
before_action :set_body_classes before_action :set_body_classes
before_action :set_confirmation_user!, only: [:show, :confirm_captcha] 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 :extend_csp_for_captcha!, only: [:show, :confirm_captcha]
before_action :require_captcha_if_needed!, only: [:show] before_action :require_captcha_if_needed!, only: [:show]
@ -65,10 +65,12 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController
@confirmation_user.nil? || @confirmation_user.confirmed? @confirmation_user.nil? || @confirmation_user.confirmed?
end end
def require_unconfirmed! def redirect_confirmed_user
if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
redirect_to(current_user.approved? ? root_path : edit_user_registration_path) redirect_to(current_user.approved? ? root_path : edit_user_registration_path)
end end
def signed_in_confirmed_user?
user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank?
end end
def set_body_classes def set_body_classes

View file

@ -2,7 +2,7 @@
class Auth::PasswordsController < Devise::PasswordsController class Auth::PasswordsController < Devise::PasswordsController
skip_before_action :check_self_destruct! 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 before_action :set_body_classes
layout 'auth' layout 'auth'
@ -19,12 +19,10 @@ class Auth::PasswordsController < Devise::PasswordsController
private private
def check_validity_of_reset_password_token def redirect_invalid_reset_token
unless reset_password_token_is_valid?
flash[:error] = I18n.t('auth.invalid_reset_password_token') flash[:error] = I18n.t('auth.invalid_reset_password_token')
redirect_to new_password_path(resource_name) redirect_to new_password_path(resource_name)
end end
end
def set_body_classes def set_body_classes
@body_classes = 'lighter' @body_classes = 'lighter'

View file

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
class Redirect::AccountsController < ApplicationController class Redirect::AccountsController < Redirect::BaseController
private private
def set_resource def set_resource

View file

@ -6,8 +6,8 @@ module Settings
skip_before_action :check_self_destruct! skip_before_action :check_self_destruct!
skip_before_action :require_functional! skip_before_action :require_functional!
before_action :require_otp_enabled before_action :redirect_invalid_otp, unless: -> { current_user.otp_enabled? }
before_action :require_webauthn_enabled, only: [:index, :destroy] before_action :redirect_invalid_webauthn, only: [:index, :destroy], unless: -> { current_user.webauthn_enabled? }
def index; end def index; end
def new; end def new; end
@ -85,19 +85,15 @@ module Settings
private private
def require_otp_enabled def redirect_invalid_otp
unless current_user.otp_enabled?
flash[:error] = t('webauthn_credentials.otp_required') flash[:error] = t('webauthn_credentials.otp_required')
redirect_to settings_two_factor_authentication_methods_path redirect_to settings_two_factor_authentication_methods_path
end end
end
def require_webauthn_enabled def redirect_invalid_webauthn
unless current_user.webauthn_enabled?
flash[:error] = t('webauthn_credentials.not_enabled') flash[:error] = t('webauthn_credentials.not_enabled')
redirect_to settings_two_factor_authentication_methods_path redirect_to settings_two_factor_authentication_methods_path
end end
end end
end end
end
end end

View 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

View file

@ -9,7 +9,11 @@ exports[`<AutosuggestEmoji /> renders emoji with custom url 1`] = `
className="emojione" className="emojione"
src="http://example.com/emoji.png" src="http://example.com/emoji.png"
/> />
<div
className="autosuggest-emoji__name"
>
:foobar: :foobar:
</div>
</div> </div>
`; `;
@ -22,6 +26,10 @@ exports[`<AutosuggestEmoji /> renders native emoji 1`] = `
className="emojione" className="emojione"
src="/emoji/1f499.svg" src="/emoji/1f499.svg"
/> />
<div
className="autosuggest-emoji__name"
>
:foobar: :foobar:
</div>
</div> </div>
`; `;

View file

@ -37,10 +37,10 @@ class Account extends ImmutablePureComponent {
static propTypes = { static propTypes = {
size: PropTypes.number, size: PropTypes.number,
account: ImmutablePropTypes.record, account: ImmutablePropTypes.record,
onFollow: PropTypes.func.isRequired, onFollow: PropTypes.func,
onBlock: PropTypes.func.isRequired, onBlock: PropTypes.func,
onMute: PropTypes.func.isRequired, onMute: PropTypes.func,
onMuteNotifications: PropTypes.func.isRequired, onMuteNotifications: PropTypes.func,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
hidden: PropTypes.bool, hidden: PropTypes.bool,
hideButtons: PropTypes.bool, hideButtons: PropTypes.bool,

View file

@ -35,7 +35,7 @@ export default class AutosuggestEmoji extends PureComponent {
alt={emoji.native || emoji.colons} alt={emoji.native || emoji.colons}
/> />
{emoji.colons} <div className='autosuggest-emoji__name'>{emoji.colons}</div>
</div> </div>
); );
} }

View file

@ -1,5 +1,3 @@
import { FormattedMessage } from 'react-intl';
import { ShortNumber } from 'mastodon/components/short_number'; import { ShortNumber } from 'mastodon/components/short_number';
interface Props { interface Props {
@ -16,27 +14,18 @@ interface Props {
}; };
} }
export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => { 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'>
<div className='autosuggest-hashtag__name'> <div className='autosuggest-hashtag__name'>
#<strong>{tag.name}</strong> #<strong>{tag.name}</strong>
</div> </div>
{tag.history !== undefined && ( {tag.history !== undefined && (
<div className='autosuggest-hashtag__uses'> <div className='autosuggest-hashtag__uses'>
<FormattedMessage <ShortNumber
id='autosuggest_hashtag.per_week' value={tag.history.reduce((total, day) => total + day.uses * 1, 0)}
defaultMessage='{count} per week'
values={{ count: weeklyUses }}
/> />
</div> </div>
)} )}
</div> </div>
); );
};

View file

@ -5,6 +5,8 @@ import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Overlay from 'react-overlays/Overlay';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
import AutosuggestEmoji from './autosuggest_emoji'; import AutosuggestEmoji from './autosuggest_emoji';
@ -195,9 +197,6 @@ export default class AutosuggestInput extends ImmutablePureComponent {
return ( return (
<div className='autosuggest-input'> <div className='autosuggest-input'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<input <input
type='text' type='text'
ref={this.setInput} ref={this.setInput}
@ -212,18 +211,24 @@ export default class AutosuggestInput extends ImmutablePureComponent {
onBlur={this.onBlur} onBlur={this.onBlur}
dir='auto' dir='auto'
aria-autocomplete='list' aria-autocomplete='list'
aria-label={placeholder}
id={id} id={id}
className={className} className={className}
maxLength={maxLength} maxLength={maxLength}
lang={lang} lang={lang}
spellCheck={spellCheck} spellCheck={spellCheck}
/> />
</label>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> <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)} {suggestions.map(this.renderSuggestion)}
</div> </div>
</div> </div>
)}
</Overlay>
</div>
); );
} }

View file

@ -5,6 +5,7 @@ import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import Overlay from 'react-overlays/Overlay';
import Textarea from 'react-textarea-autosize'; import Textarea from 'react-textarea-autosize';
import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container';
@ -52,7 +53,6 @@ const AutosuggestTextarea = forwardRef(({
onFocus, onFocus,
autoFocus = true, autoFocus = true,
lang, lang,
children,
}, textareaRef) => { }, textareaRef) => {
const [suggestionsHidden, setSuggestionsHidden] = useState(true); const [suggestionsHidden, setSuggestionsHidden] = useState(true);
@ -183,12 +183,8 @@ const AutosuggestTextarea = forwardRef(({
); );
}; };
return [ return (
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
<div className='autosuggest-textarea'> <div className='autosuggest-textarea'>
<label>
<span style={{ display: 'none' }}>{placeholder}</span>
<Textarea <Textarea
ref={textareaRef} ref={textareaRef}
className='autosuggest-textarea__textarea' className='autosuggest-textarea__textarea'
@ -204,19 +200,21 @@ const AutosuggestTextarea = forwardRef(({
onPaste={handlePaste} onPaste={handlePaste}
dir='auto' dir='auto'
aria-autocomplete='list' aria-autocomplete='list'
aria-label={placeholder}
lang={lang} lang={lang}
/> />
</label>
</div>
{children}
</div>,
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'> <Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={textareaRef} popperConfig={{ strategy: 'fixed' }}>
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> {({ props }) => (
<div {...props}>
<div className='autosuggest-textarea__suggestions' style={{ width: textareaRef.current?.clientWidth }}>
{suggestions.map(renderSuggestion)} {suggestions.map(renderSuggestion)}
</div> </div>
</div>, </div>
]; )}
</Overlay>
</div>
);
}); });
AutosuggestTextarea.propTypes = { AutosuggestTextarea.propTypes = {
@ -232,7 +230,6 @@ AutosuggestTextarea.propTypes = {
onKeyDown: PropTypes.func, onKeyDown: PropTypes.func,
onPaste: PropTypes.func.isRequired, onPaste: PropTypes.func.isRequired,
onFocus:PropTypes.func, onFocus:PropTypes.func,
children: PropTypes.node,
autoFocus: PropTypes.bool, autoFocus: PropTypes.bool,
lang: PropTypes.string, lang: PropTypes.string,
}; };

View file

@ -165,7 +165,7 @@ class Dropdown extends PureComponent {
children: PropTypes.node, children: PropTypes.node,
icon: PropTypes.string, icon: PropTypes.string,
iconComponent: PropTypes.func, iconComponent: PropTypes.func,
items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired, items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]),
loading: PropTypes.bool, loading: PropTypes.bool,
size: PropTypes.number, size: PropTypes.number,
title: PropTypes.string, title: PropTypes.string,

View file

@ -76,6 +76,10 @@ export const defaultMediaVisibility = (status) => {
}; };
const messages = defineMessages({ 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}' }, edited: { id: 'status.edited', defaultMessage: 'Edited {date}' },
}); });

View file

@ -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 MutualIcon from '@/material-icons/400-24px/compare_arrows.svg?react';
import LoginIcon from '@/material-icons/400-24px/key.svg?react'; import LoginIcon from '@/material-icons/400-24px/key.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.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 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 ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import LimitedIcon from '@/material-icons/400-24px/shield.svg?react'; import LimitedIcon from '@/material-icons/400-24px/shield.svg?react';
import PersonalIcon from '@/material-icons/400-24px/sticky_note.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', id: 'privacy.public_unlisted.short',
defaultMessage: 'Public unlisted', defaultMessage: 'Public unlisted',
}, },
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' }, login_short: {
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, id: 'privacy.login.short',
defaultMessage: 'Login only',
},
unlisted_short: {
id: 'privacy.unlisted.short',
defaultMessage: 'Quiet public',
},
private_short: { private_short: {
id: 'privacy.private.short', id: 'privacy.private.short',
defaultMessage: 'Followers only', defaultMessage: 'Followers',
}, },
limited_short: { limited_short: {
id: 'privacy.limited.short', id: 'privacy.limited.short',
@ -61,7 +67,7 @@ const messages = defineMessages({
}, },
direct_short: { direct_short: {
id: 'privacy.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: { unlisted: {
icon: 'unlock', icon: 'unlock',
iconComponent: LockOpenIcon, iconComponent: QuietTimeIcon,
text: intl.formatMessage(messages.unlisted_short), text: intl.formatMessage(messages.unlisted_short),
}, },
private: { private: {

View file

@ -1,14 +1,12 @@
import { PureComponent } from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { fetchCustomEmojis } from '../actions/custom_emojis'; import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
import { hydrateStore } from '../actions/store'; import { hydrateStore } from 'mastodon/actions/store';
import Compose from '../features/standalone/compose'; import { Router } from 'mastodon/components/router';
import initialState from '../initial_state'; import Compose from 'mastodon/features/standalone/compose';
import { IntlProvider } from '../locales'; import initialState from 'mastodon/initial_state';
import { store } from '../store'; import { IntlProvider } from 'mastodon/locales';
import { store } from 'mastodon/store';
if (initialState) { if (initialState) {
store.dispatch(hydrateStore(initialState)); store.dispatch(hydrateStore(initialState));
@ -16,16 +14,14 @@ if (initialState) {
store.dispatch(fetchCustomEmojis()); store.dispatch(fetchCustomEmojis());
export default class ComposeContainer extends PureComponent { const ComposeContainer = () => (
render () {
return (
<IntlProvider> <IntlProvider>
<Provider store={store}> <Provider store={store}>
<Router>
<Compose /> <Compose />
</Router>
</Provider> </Provider>
</IntlProvider> </IntlProvider>
); );
}
} export default ComposeContainer;

View file

@ -1,13 +1,13 @@
import PropTypes from 'prop-types'; import { useCallback } from 'react';
import { PureComponent } 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 MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { openModal } from 'mastodon/actions/modal';
import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { logOut } from 'mastodon/utils/log_out';
const messages = defineMessages({ const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
@ -25,22 +25,25 @@ const messages = defineMessages({
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, 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 = { const handleLogoutClick = useCallback(() => {
account: ImmutablePropTypes.record.isRequired, dispatch(openModal({
onLogout: PropTypes.func.isRequired, modalType: 'CONFIRM',
intl: PropTypes.object.isRequired, modalProps: {
}; message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
handleLogout = () => { closeWhenConfirm: false,
this.props.onLogout(); onConfirm: () => logOut(),
}; },
}));
render () { }, [dispatch, intl]);
const { intl } = this.props;
let menu = []; let menu = [];
@ -50,8 +53,8 @@ class ActionBar extends PureComponent {
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); 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.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.emoji_reactions), to: '/emoji_reactions' });
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' }); menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' });
@ -61,17 +64,15 @@ class ActionBar extends PureComponent {
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' }); menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
menu.push(null); menu.push(null);
menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout }); menu.push({ text: intl.formatMessage(messages.logout), action: handleLogoutClick });
return ( return (
<div className='compose__action-bar'> <DropdownMenuContainer
<div className='compose__action-bar-dropdown'> items={menu}
<DropdownMenuContainer items={menu} icon='bars' iconComponent={MenuIcon} size={24} direction='right' /> icon='bars'
</div> iconComponent={MoreHorizIcon}
</div> size={24}
direction='right'
/>
); );
} };
}
export default injectIntl(ActionBar);

View file

@ -15,7 +15,7 @@ export default class AutosuggestAccount extends ImmutablePureComponent {
return ( return (
<div className='autosuggest-account' title={account.get('acct')}> <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} /> <DisplayName account={account} />
</div> </div>
); );

View file

@ -1,26 +1,18 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { length } from 'stringz'; 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) { if (diff < 0) {
return <span className='character-counter character-counter--over'>{diff}</span>; return <span className='character-counter character-counter--over'>{diff}</span>;
} }
return <span className='character-counter'>{diff}</span>; return <span className='character-counter'>{diff}</span>;
} };
render () { CharacterCounter.propTypes = {
const diff = this.props.max - length(this.props.text); text: PropTypes.string.isRequired,
return this.checkRemainingText(diff); max: PropTypes.number.isRequired,
} };
}

View file

@ -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);

View file

@ -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);

View file

@ -10,23 +10,19 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz'; 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 { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
import AutosuggestInput from '../../../components/autosuggest_input'; import AutosuggestInput from '../../../components/autosuggest_input';
import AutosuggestTextarea from '../../../components/autosuggest_textarea'; import AutosuggestTextarea from '../../../components/autosuggest_textarea';
import { Button } from '../../../components/button'; 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 EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import ExpirationDropdownContainer from '../containers/expiration_dropdown_container'; import ExpirationDropdownContainer from '../containers/expiration_dropdown_container';
import LanguageDropdown from '../containers/language_dropdown_container'; import LanguageDropdown from '../containers/language_dropdown_container';
import MarkdownButtonContainer from '../containers/markdown_button_container'; import MarkdownButtonContainer from '../containers/markdown_button_container';
import PollButtonContainer from '../containers/poll_button_container'; import PollButtonContainer from '../containers/poll_button_container';
import PollFormContainer from '../containers/poll_form_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import ReplyIndicatorContainer from '../containers/reply_indicator_container';
import SearchabilityDropdownContainer from '../containers/searchability_dropdown_container'; import SearchabilityDropdownContainer from '../containers/searchability_dropdown_container';
import SpoilerButtonContainer from '../containers/spoiler_button_container'; import SpoilerButtonContainer from '../containers/spoiler_button_container';
import UploadButtonContainer from '../containers/upload_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 WarningContainer from '../containers/warning_container';
import { countableText } from '../util/counter'; 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 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({ const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' }, spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning (optional)' },
publish: { id: 'compose_form.publish', defaultMessage: 'Publish' }, publish: { id: 'compose_form.publish', defaultMessage: 'Post' },
publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' }, saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Update' },
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' }, reply: { id: 'compose_form.reply', defaultMessage: 'Reply' },
}); });
class ComposeForm extends ImmutablePureComponent { class ComposeForm extends ImmutablePureComponent {
@ -72,6 +72,7 @@ class ComposeForm extends ImmutablePureComponent {
onPickEmoji: PropTypes.func.isRequired, onPickEmoji: PropTypes.func.isRequired,
onPickExpiration: PropTypes.func.isRequired, onPickExpiration: PropTypes.func.isRequired,
autoFocus: PropTypes.bool, autoFocus: PropTypes.bool,
withoutNavigation: PropTypes.bool,
anyMedia: PropTypes.bool, anyMedia: PropTypes.bool,
isInReply: PropTypes.bool, isInReply: PropTypes.bool,
singleColumn: PropTypes.bool, singleColumn: PropTypes.bool,
@ -237,35 +238,30 @@ class ComposeForm extends ImmutablePureComponent {
}; };
render () { render () {
const { intl, onPaste, autoFocus } = this.props; const { intl, onPaste, autoFocus, withoutNavigation } = this.props;
const { highlighted } = this.state; const { highlighted } = this.state;
const disabled = this.props.isSubmitting; 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 ( return (
<form className='compose-form' onSubmit={this.handleSubmit}> <form className='compose-form' onSubmit={this.handleSubmit}>
<ReplyIndicator />
{!withoutNavigation && <NavigationBar />}
<WarningContainer /> <WarningContainer />
<ReplyIndicatorContainer /> <div className={classNames('compose-form__highlightable', { active: highlighted })} ref={this.setRef}>
<div className='compose-form__scrollable'>
<EditIndicator />
{this.props.spoiler && (
<div className='spoiler-input'>
<div className='spoiler-input__border' />
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
<AutosuggestInput <AutosuggestInput
placeholder={intl.formatMessage(messages.spoiler_placeholder)} placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText} value={this.props.spoilerText}
disabled={disabled}
onChange={this.handleChangeSpoilerText} onChange={this.handleChangeSpoilerText}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
disabled={!this.props.spoiler}
ref={this.setSpoilerText} ref={this.setSpoilerText}
suggestions={this.props.suggestions} suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
@ -277,9 +273,11 @@ class ComposeForm extends ImmutablePureComponent {
lang={this.props.lang} lang={this.props.lang}
spellCheck spellCheck
/> />
</div>
<div className={classNames('compose-form__highlightable', { active: highlighted })}> <div className='spoiler-input__border' />
</div>
)}
<AutosuggestTextarea <AutosuggestTextarea
ref={this.textareaRef} ref={this.textareaRef}
placeholder={intl.formatMessage(messages.placeholder)} placeholder={intl.formatMessage(messages.placeholder)}
@ -295,45 +293,46 @@ class ComposeForm extends ImmutablePureComponent {
onPaste={onPaste} onPaste={onPaste}
autoFocus={autoFocus} autoFocus={autoFocus}
lang={this.props.lang} lang={this.props.lang}
> />
<div className='compose-form__modifiers'>
<UploadFormContainer />
<PollFormContainer />
</div> </div>
</AutosuggestTextarea>
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
<ExpirationDropdownContainer onPickExpiration={this.handleExpirationPick} />
<div className='compose-form__buttons-wrapper'> <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'> <div className='compose-form__buttons'>
<UploadButtonContainer /> <UploadButtonContainer />
<PollButtonContainer /> <PollButtonContainer />
<PrivacyDropdownContainer disabled={this.props.isEditing} />
<SearchabilityDropdownContainer disabled={this.props.isEditing} />
<SpoilerButtonContainer /> <SpoilerButtonContainer />
<LanguageDropdown /> <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />
<MarkdownButtonContainer /> <MarkdownButtonContainer />
</div>
<div className='character-counter__wrapper'>
<CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} /> <CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} />
</div> </div>
</div>
</div>
<CircleSelectContainer />
<div className='compose-form__publish'> <div className='compose-form__submit'>
<div className='compose-form__publish-button-wrapper'>
<Button <Button
type='submit' type='submit'
text={publishText} text={intl.formatMessage(this.props.isEditing ? messages.saveChanges : (this.props.isInReply ? messages.reply : messages.publish))}
disabled={!this.canSubmit()} disabled={!this.canSubmit()}
block
/> />
</div> </div>
</div> </div>
</div>
</div>
</form> </form>
); );
} }

View file

@ -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>
);
};

View file

@ -10,6 +10,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay'; 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 { assetHost } from 'mastodon/utils/config';
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';
@ -321,7 +323,6 @@ class EmojiPickerDropdown extends PureComponent {
onPickEmoji: PropTypes.func.isRequired, onPickEmoji: PropTypes.func.isRequired,
onSkinTone: PropTypes.func.isRequired, onSkinTone: PropTypes.func.isRequired,
skinTone: PropTypes.number.isRequired, skinTone: PropTypes.number.isRequired,
button: PropTypes.node,
}; };
state = { state = {
@ -398,23 +399,24 @@ class EmojiPickerDropdown extends PureComponent {
}; };
render () { 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 title = intl.formatMessage(messages.emoji);
const { active, loading, bottom } = this.state; const { active, loading, bottom } = this.state;
return ( return (
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}> <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown} ref={this.setTargetRef}>
<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}> <IconButton
{button || <img title={title}
className={classNames('emojione', { 'pulse-loading': active && loading })} aria-expanded={active}
alt='🙂' active={active}
src={`${assetHost}/emoji/1f642.svg`} iconComponent={MoodIcon}
/>} onClick={this.onToggle}
</div> inverted
/>
<Overlay show={active} placement={ bottom ? 'bottom' : 'top' } target={this.findTarget} popperConfig={{ strategy: 'fixed' }}> <Overlay show={active} placement={ bottom ? 'bottom' : 'top' } target={this.findTarget} popperConfig={{ strategy: 'fixed' }}>
{({ props, placement })=> ( {({ props, placement })=> (
<div {...props} style={{ ...props.style, width: 299 }}> <div {...props} style={{ ...props.style }}>
<div className={`dropdown-animation ${placement}`}> <div className={`dropdown-animation ${placement}`}>
<EmojiPickerMenu <EmojiPickerMenu
custom_emojis={this.props.custom_emojis} custom_emojis={this.props.custom_emojis}

View file

@ -9,8 +9,7 @@ import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay'; import Overlay from 'react-overlays/Overlay';
import TimerIcon from '@/material-icons/400-24px/timer.svg?react'; import TimerIcon from '@/material-icons/400-24px/timer.svg?react';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from '../../../components/icon_button';
const messages = defineMessages({ const messages = defineMessages({
add_expiration: { id: 'status.expiration.add', defaultMessage: 'Set status expiration' }, add_expiration: { id: 'status.expiration.add', defaultMessage: 'Set status expiration' },
@ -23,7 +22,6 @@ class ExpirationDropdownMenu extends PureComponent {
static propTypes = { static propTypes = {
style: PropTypes.object, style: PropTypes.object,
items: PropTypes.array.isRequired, items: PropTypes.array.isRequired,
value: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
}; };
@ -108,13 +106,13 @@ class ExpirationDropdownMenu extends PureComponent {
}; };
render () { render () {
const { style, items, value } = this.props; const { style, items } = this.props;
return ( return (
<div style={{ ...style }} role='listbox' ref={this.setRef}> <div style={{ ...style }} role='listbox' ref={this.setRef}>
{items.map(item => ( {items.map(item => (
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}> <div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option')} aria-selected={false} ref={null}>
<div className='expiration-dropdown__option__content'> <div className='privacy-dropdown__option__content'>
<strong>{item.text}</strong> <strong>{item.text}</strong>
</div> </div>
</div> </div>
@ -131,7 +129,6 @@ class ExpirationDropdown extends PureComponent {
isUserTouching: PropTypes.func, isUserTouching: PropTypes.func,
onModalOpen: PropTypes.func, onModalOpen: PropTypes.func,
onModalClose: PropTypes.func, onModalClose: PropTypes.func,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired,
noDirect: PropTypes.bool, noDirect: PropTypes.bool,
container: PropTypes.func, container: PropTypes.func,
@ -145,21 +142,11 @@ class ExpirationDropdown extends PureComponent {
}; };
handleToggle = () => { 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) { if (this.state.open && this.activeElement) {
this.activeElement.focus({ preventScroll: true }); this.activeElement.focus({ preventScroll: true });
} }
this.setState({ open: !this.state.open }); this.setState({ open: !this.state.open });
}
}; };
handleModalActionClick = (e) => { handleModalActionClick = (e) => {
@ -230,36 +217,30 @@ class ExpirationDropdown extends PureComponent {
}; };
render () { render () {
const { value, container, disabled, intl } = this.props; const { container, disabled, intl } = this.props;
const { open, placement } = this.state; const { open, placement } = this.state;
return ( return (
<div className={classNames('expiration-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}> <div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
<div className={classNames('expiration-dropdown__value')} ref={this.setTargetRef}> <button
<IconButton type='button'
className='expiration-dropdown__value-icon'
icon='clock-o'
iconComponent={TimerIcon}
title={intl.formatMessage(messages.add_expiration)} title={intl.formatMessage(messages.add_expiration)}
size={18} aria-expanded={open}
expanded={open}
active={open}
inverted
onClick={this.handleToggle} onClick={this.handleToggle}
onMouseDown={this.handleMouseDown} onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown} onKeyDown={this.handleButtonKeyDown}
style={{ height: null, lineHeight: '27px' }}
disabled={disabled} disabled={disabled}
/> className={classNames('dropdown-button', { active: open })}
</div> >
<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 }) => ( {({ props, placement }) => (
<div {...props}> <div {...props}>
<div className={`dropdown-animation expiration-dropdown__dropdown ${placement}`}> <div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
<ExpirationDropdownMenu <ExpirationDropdownMenu
items={this.options} items={this.options}
value={value}
onClose={this.handleClose} onClose={this.handleClose}
onChange={this.handleChange} onChange={this.handleChange}
/> />

View file

@ -9,10 +9,11 @@ import { supportsPassiveEvents } from 'detect-passive-events';
import fuzzysort from 'fuzzysort'; import fuzzysort from 'fuzzysort';
import Overlay from 'react-overlays/Overlay'; 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 { languages as preloadedLanguages } from 'mastodon/initial_state';
import { loupeIcon, deleteIcon } from 'mastodon/utils/icons';
import TextIconButton from './text_icon_button';
const messages = defineMessages({ const messages = defineMessages({
changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' }, changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' },
@ -231,7 +232,7 @@ class LanguageDropdownMenu extends PureComponent {
<div ref={this.setRef}> <div ref={this.setRef}>
<div className='emoji-mart-search'> <div className='emoji-mart-search'>
<input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.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>
<div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}> <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}>
@ -297,20 +298,24 @@ class LanguageDropdown extends PureComponent {
render () { render () {
const { value, intl, frequentlyUsedLanguages } = this.props; const { value, intl, frequentlyUsedLanguages } = this.props;
const { open, placement } = this.state; const { open, placement } = this.state;
const current = preloadedLanguages.find(lang => lang[0] === value) ?? [];
return ( return (
<div className={classNames('privacy-dropdown', placement, { active: open })}> <div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
<div className='privacy-dropdown__value' ref={this.setTargetRef} > <button
<TextIconButton type='button'
className='privacy-dropdown__value-icon'
label={value && value.toUpperCase()}
title={intl.formatMessage(messages.changeLanguage)} title={intl.formatMessage(messages.changeLanguage)}
active={open} aria-expanded={open}
onClick={this.handleToggle} onClick={this.handleToggle}
/> onMouseDown={this.handleMouseDown}
</div> 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 }) => ( {({ props, placement }) => (
<div {...props}> <div {...props}>
<div className={`dropdown-animation language-dropdown__dropdown ${placement}`} > <div className={`dropdown-animation language-dropdown__dropdown ${placement}`} >

View file

@ -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 CloseIcon from 'mastodon/../material-icons/400-24px/close.svg?react';
import ImmutablePureComponent from 'react-immutable-pure-component'; 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 = { export const NavigationBar = () => {
account: ImmutablePropTypes.record.isRequired, const dispatch = useDispatch();
onLogout: PropTypes.func.isRequired, const intl = useIntl();
onClose: PropTypes.func, const account = useSelector(state => state.getIn(['accounts', me]));
}; const isReplying = useSelector(state => !!state.getIn(['compose', 'in_reply_to']));
const handleCancelClick = useCallback(() => {
dispatch(cancelReplyCompose());
}, [dispatch]);
render () {
const username = this.props.account.get('acct');
return ( return (
<div className='navigation-bar'> <div className='navigation-bar'>
<Link to={`/@${username}`}> <Account account={account} minimal />
<span style={{ display: 'none' }}>{username}</span> {isReplying ? <IconButton title={intl.formatMessage(messages.cancel)} iconComponent={CloseIcon} onClick={handleCancelClick} /> : <ActionBar />}
<Avatar account={this.props.account} size={46} />
</Link>
<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> </div>
); );
} };
}

View file

@ -3,11 +3,10 @@ import { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl'; 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'; import { IconButton } from '../../../components/icon_button';
const messages = defineMessages({ const messages = defineMessages({
add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' }, add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' },
remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' }, remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' },
@ -22,7 +21,6 @@ class PollButton extends PureComponent {
static propTypes = { static propTypes = {
disabled: PropTypes.bool, disabled: PropTypes.bool,
unavailable: PropTypes.bool,
active: PropTypes.bool, active: PropTypes.bool,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
@ -33,17 +31,13 @@ class PollButton extends PureComponent {
}; };
render () { render () {
const { intl, active, unavailable, disabled } = this.props; const { intl, active, disabled } = this.props;
if (unavailable) {
return null;
}
return ( return (
<div className='compose-form__poll-button'> <div className='compose-form__poll-button'>
<IconButton <IconButton
icon='tasks' icon='tasks'
iconComponent={InsertChartIcon} iconComponent={BarChart4BarsIcon}
title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)} title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)}
disabled={disabled} disabled={disabled}
onClick={this.handleClick} onClick={this.handleClick}

View file

@ -1,97 +1,85 @@
import PropTypes from 'prop-types'; 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 classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes'; import { useDispatch, useSelector } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import AddIcon from '@/material-icons/400-24px/add.svg?react'; import {
import CloseIcon from '@/material-icons/400-24px/close.svg?react'; changePollSettings,
changePollOption,
clearComposeSuggestions,
fetchComposeSuggestions,
selectComposeSuggestion,
} from 'mastodon/actions/compose';
import AutosuggestInput from 'mastodon/components/autosuggest_input'; import AutosuggestInput from 'mastodon/components/autosuggest_input';
import { Icon } from 'mastodon/components/icon';
import { IconButton } from 'mastodon/components/icon_button';
const messages = defineMessages({ const messages = defineMessages({
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' }, option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Option {number}' },
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' }, add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add option' },
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' }, remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this option' },
poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' }, 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' }, 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' }, 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}}' }, minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, 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 = { <select className='compose-form__poll__select__value' value={value} onChange={onChange}>
title: PropTypes.string.isRequired, {options.map((option, i) => (
lang: PropTypes.string, <option key={i} value={option.value}>{option.label}</option>
index: PropTypes.number.isRequired, ))}
isPollMultiple: PropTypes.bool, </select>
autoFocus: PropTypes.bool, </label>
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,
};
handleOptionTitleChange = e => { Select.propTypes = {
this.props.onChange(this.props.index, e.target.value); label: PropTypes.node,
}; value: PropTypes.any,
onChange: PropTypes.func,
options: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.node,
value: PropTypes.any,
})),
};
handleOptionRemove = () => { const Option = ({ multipleChoice, index, title, autoFocus }) => {
this.props.onRemove(this.props.index); 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 => { const handleSuggestionsFetchRequested = useCallback(token => {
this.props.onToggleMultiple(); dispatch(fetchComposeSuggestions(token));
e.preventDefault(); }, [dispatch]);
e.stopPropagation();
};
handleCheckboxKeypress = e => { const handleSuggestionsClearRequested = useCallback(() => {
if (e.key === 'Enter' || e.key === ' ') { dispatch(clearComposeSuggestions());
this.handleToggleMultiple(e); }, [dispatch]);
}
};
onSuggestionsClearRequested = () => { const handleSuggestionSelected = useCallback((tokenStart, token, value) => {
this.props.onClearSuggestions(); dispatch(selectComposeSuggestion(tokenStart, token, value, ['poll', 'options', index]));
}; }, [dispatch, index]);
onSuggestionsFetchRequested = (token) => {
this.props.onFetchSuggestions(token);
};
onSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
};
render () {
const { isPollMultiple, title, lang, index, autoFocus, intl } = this.props;
return ( return (
<li> <label className={classNames('poll__option editable', { empty: index > 1 && title.length === 0 })}>
<label className='poll__option editable'> <span className={classNames('poll__input', { checkbox: multipleChoice })} />
<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)}
/>
<AutosuggestInput <AutosuggestInput
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })} placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
@ -99,91 +87,76 @@ class OptionIntl extends PureComponent {
value={title} value={title}
lang={lang} lang={lang}
spellCheck spellCheck
onChange={this.handleOptionTitleChange} onChange={handleChange}
suggestions={this.props.suggestions} suggestions={suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionsClearRequested={handleSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected} onSuggestionSelected={handleSuggestionSelected}
searchTokens={[':']} searchTokens={[':']}
autoFocus={autoFocus} autoFocus={autoFocus}
/> />
</label> </label>
<div className='poll__cancel'>
<IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' iconComponent={CloseIcon} onClick={this.handleOptionRemove} />
</div>
</li>
); );
} };
} Option.propTypes = {
title: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
multipleChoice: PropTypes.bool,
autoFocus: PropTypes.bool,
};
const Option = injectIntl(OptionIntl); 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');
class PollForm extends ImmutablePureComponent { const handleDurationChange = useCallback(({ target: { value } }) => {
dispatch(changePollSettings(value, isMultiple));
}, [dispatch, isMultiple]);
static propTypes = { const handleTypeChange = useCallback(({ target: { value } }) => {
options: ImmutablePropTypes.list, dispatch(changePollSettings(expiresIn, value === 'true'));
lang: PropTypes.string, }, [dispatch, expiresIn]);
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 = () => { if (poll === null) {
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; return null;
} }
const autoFocusIndex = options.indexOf('');
return ( return (
<div className='compose-form__poll-wrapper'> <div className='compose-form__poll'>
<ul> {options.map((title, i) => (
{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} />)} <Option
</ul> title={title}
key={i}
index={i}
multipleChoice={isMultiple}
autoFocus={i === 0}
/>
))}
<div className='poll__footer'> <div className='compose-form__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> <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} />
{/* eslint-disable-next-line jsx-a11y/no-onchange */} <div className='compose-form__poll__footer__sep' />
<select value={expiresIn} onChange={this.handleSelectDuration}>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option> <Select label={intl.formatMessage(messages.type)} options={[
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option> { value: false, label: intl.formatMessage(messages.singleChoice) },
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option> { value: true, label: intl.formatMessage(messages.multipleChoice) },
<option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option> ]} value={isMultiple} onChange={handleTypeChange} />
<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> </div>
</div> </div>
); );
} };
}
export default injectIntl(PollForm);

View file

@ -5,7 +5,6 @@ import { injectIntl, defineMessages } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
import Overlay from 'react-overlays/Overlay'; 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 AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import PublicUnlistedIcon from '@/material-icons/400-24px/cloud.svg?react'; import PublicUnlistedIcon from '@/material-icons/400-24px/cloud.svg?react';
import MutualIcon from '@/material-icons/400-24px/compare_arrows.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 LoginIcon from '@/material-icons/400-24px/key.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.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 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 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 { Icon } from 'mastodon/components/icon';
import { enableLoginPrivacy, enableLocalPrivacy } from 'mastodon/initial_state'; import { enableLoginPrivacy, enableLocalPrivacy } from 'mastodon/initial_state';
import { IconButton } from '../../../components/icon_button';
const messages = defineMessages({ const messages = defineMessages({
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all' }, public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' },
unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' },
unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but opted-out of discovery features' }, unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Fewer algorithmic fanfares' },
public_unlisted_short: { id: 'privacy.public_unlisted.short', defaultMessage: 'Public unlisted' }, 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' }, public_unlisted_long: { id: 'privacy.public_unlisted.long', defaultMessage: 'Visible for all without GTL' },
login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' }, login_short: { id: 'privacy.login.short', defaultMessage: 'Login only' },
login_long: { id: 'privacy.login.long', defaultMessage: 'Login user only' }, login_long: { id: 'privacy.login.long', defaultMessage: 'Login user only' },
private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, private_short: { id: 'privacy.private.short', defaultMessage: 'Followers' },
private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, 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_short: { id: 'privacy.mutual.short', defaultMessage: 'Mutual' },
mutual_long: { id: 'privacy.mutual.long', defaultMessage: 'Mutual follows only' }, mutual_long: { id: 'privacy.mutual.long', defaultMessage: 'Mutual follows only' },
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle' }, circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle' },
circle_long: { id: 'privacy.circle.long', defaultMessage: 'Circle members only' }, circle_long: { id: 'privacy.circle.long', defaultMessage: 'Circle members only' },
reply_short: { id: 'privacy.reply.short', defaultMessage: 'Reply' }, reply_short: { id: 'privacy.reply.short', defaultMessage: 'Reply' },
reply_long: { id: 'privacy.reply.long', defaultMessage: 'Reply to limited post' }, reply_long: { id: 'privacy.reply.long', defaultMessage: 'Reply to limited post' },
direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, direct_short: { id: 'privacy.direct.short', defaultMessage: 'Specific people' },
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, direct_long: { id: 'privacy.direct.long', defaultMessage: 'Everyone mentioned in the post' },
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, 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; const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
@ -151,6 +152,12 @@ class PrivacyDropdownMenu extends PureComponent {
<strong>{item.text}</strong> <strong>{item.text}</strong>
{item.meta} {item.meta}
</div> </div>
{item.extra && (
<div className='privacy-dropdown__option__additional' title={item.extra}>
<Icon id='info-circle' icon={item.extraIcomComponent ?? InfoIcon} />
</div>
)}
</div> </div>
))} ))}
</div> </div>
@ -181,30 +188,11 @@ class PrivacyDropdown extends PureComponent {
}; };
handleToggle = () => { 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) { if (this.state.open && this.activeElement) {
this.activeElement.focus({ preventScroll: true }); this.activeElement.focus({ preventScroll: true });
} }
this.setState({ open: !this.state.open }); 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 => { 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: '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: '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: '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: '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: '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) }, { 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) { if (!this.props.noDirect) {
@ -294,7 +282,7 @@ class PrivacyDropdown extends PureComponent {
if (replyToLimited) { if (replyToLimited) {
if (!this.selectableOptions.some((op) => op.value === 'reply')) { if (!this.selectableOptions.some((op) => op.value === 'reply')) {
this.selectableOptions.unshift( 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 { } else {
@ -307,23 +295,21 @@ class PrivacyDropdown extends PureComponent {
return ( return (
<div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}> <div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}>
<IconButton <button
className='privacy-dropdown__value-icon' type='button'
icon={valueOption.icon}
iconComponent={valueOption.iconComponent}
title={intl.formatMessage(messages.change_privacy)} title={intl.formatMessage(messages.change_privacy)}
size={18} aria-expanded={open}
expanded={open}
active={open}
inverted
onClick={this.handleToggle} onClick={this.handleToggle}
onMouseDown={this.handleMouseDown} onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown} onKeyDown={this.handleButtonKeyDown}
style={{ height: null, lineHeight: '27px' }}
disabled={disabled} 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 }) => ( {({ props, placement }) => (
<div {...props}> <div {...props}>
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}> <div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>

View file

@ -1,44 +1,19 @@
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 { useSelector } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
import AttachmentList from 'mastodon/components/attachment_list'; import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router'; import { Avatar } from 'mastodon/components/avatar';
import { DisplayName } from 'mastodon/components/display_name';
import { Icon } from 'mastodon/components/icon';
import { Avatar } from '../../../components/avatar'; export const ReplyIndicator = () => {
import { DisplayName } from '../../../components/display_name'; const inReplyToId = useSelector(state => state.getIn(['compose', 'in_reply_to']));
import { IconButton } from '../../../components/icon_button'; 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' },
});
class ReplyIndicator extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
onCancel: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
...WithOptionalRouterPropTypes,
};
handleClick = () => {
this.props.onCancel();
};
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) { if (!status) {
return null; return null;
@ -48,27 +23,26 @@ class ReplyIndicator extends ImmutablePureComponent {
return ( return (
<div className='reply-indicator'> <div className='reply-indicator'>
<div className='reply-indicator__header'> <div className='reply-indicator__line' />
<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'> <Link to={`/@${account.get('acct')}`} className='detailed-status__display-avatar'>
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div> <Avatar account={account} size={46} />
<DisplayName account={status.get('account')} /> </Link>
</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} /> <div className='reply-indicator__content translate' dangerouslySetInnerHTML={content} />
{status.get('media_attachments').size > 0 && ( {(status.get('poll') || status.get('media_attachments').size > 0) && (
<AttachmentList <div className='reply-indicator__attachments'>
compact {status.get('poll') && <><Icon icon={BarChart4BarsIcon} /><FormattedMessage id='reply_indicator.poll' defaultMessage='Poll' /></>}
media={status.get('media_attachments')} {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> </div>
</div>
); );
} };
}
export default withOptionalRouter(injectIntl(ReplyIndicator));

View file

@ -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 LockIcon from '@/material-icons/400-24px/lock.svg?react';
import LockOpenIcon from '@/material-icons/400-24px/no_encryption.svg?react'; import LockOpenIcon from '@/material-icons/400-24px/no_encryption.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.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 { Icon } from 'mastodon/components/icon';
import { enableLocalPrivacy } from 'mastodon/initial_state'; import { enableLocalPrivacy } from 'mastodon/initial_state';
import { IconButton } from '../../../components/icon_button';
const messages = defineMessages({ const messages = defineMessages({
public_short: { id: 'searchability.public.short', defaultMessage: 'Public' }, public_short: { id: 'searchability.public.short', defaultMessage: 'Public' },
@ -168,30 +165,11 @@ class SearchabilityDropdown extends PureComponent {
}; };
handleToggle = () => { 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) { if (this.state.open && this.activeElement) {
this.activeElement.focus({ preventScroll: true }); this.activeElement.focus({ preventScroll: true });
} }
this.setState({ open: !this.state.open }); 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 => { handleKeyDown = e => {
@ -263,31 +241,22 @@ class SearchabilityDropdown extends PureComponent {
const valueOption = this.options.find(item => item.value === value); const valueOption = this.options.find(item => item.value === value);
return ( return (
<div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}> <div ref={this.setTargetRef} 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}> <button
<IconButton type='button'
className='privacy-dropdown__value-icon'
icon={valueOption.icon}
iconComponent={valueOption.iconComponent}
title={intl.formatMessage(messages.change_searchability)} title={intl.formatMessage(messages.change_searchability)}
size={18} aria-expanded={open}
expanded={open}
active={open}
inverted
onClick={this.handleToggle} onClick={this.handleToggle}
onMouseDown={this.handleMouseDown} onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown} onKeyDown={this.handleButtonKeyDown}
style={{ height: null, lineHeight: '27px' }}
disabled={disabled} disabled={disabled}
/> className={classNames('dropdown-button', { active: open })}
<Icon >
className='searchability-dropdown__value-overlay' <Icon id={valueOption.icon} icon={valueOption.iconComponent} />
id='search' <span className='dropdown-button__label'>{valueOption.text}</span>
icon={SearchIcon} </button>
/>
</div>
<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 }) => ( {({ props, placement }) => (
<div {...props}> <div {...props}>
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}> <div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>

View file

@ -2,6 +2,8 @@ import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; 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 CloseIcon from '@/material-icons/400-24px/close.svg?react';
import EditIcon from '@/material-icons/400-24px/edit.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 { Icon } from 'mastodon/components/icon';
import Motion from '../../ui/util/optional_motion'; import Motion from '../../ui/util/optional_motion';
@ -18,6 +21,7 @@ export default class Upload extends ImmutablePureComponent {
static propTypes = { static propTypes = {
media: ImmutablePropTypes.map.isRequired, media: ImmutablePropTypes.map.isRequired,
sensitive: PropTypes.bool,
onUndo: PropTypes.func.isRequired, onUndo: PropTypes.func.isRequired,
onOpenFocalPoint: PropTypes.func.isRequired, onOpenFocalPoint: PropTypes.func.isRequired,
}; };
@ -33,7 +37,7 @@ export default class Upload extends ImmutablePureComponent {
}; };
render () { render () {
const { media } = this.props; const { media, sensitive } = this.props;
if (!media) { if (!media) {
return null; return null;
@ -43,22 +47,26 @@ export default class Upload extends ImmutablePureComponent {
const focusY = media.getIn(['meta', 'focus', 'y']); const focusY = media.getIn(['meta', 'focus', 'y']);
const x = ((focusX / 2) + .5) * 100; const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100; const y = ((focusY / -2) + .5) * 100;
const missingDescription = (media.get('description') || '').length === 0;
return ( return (
<div className='compose-form__upload'> <div className='compose-form__upload'>
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
{({ scale }) => ( {({ 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'> <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 compose-form__upload__delete' onClick={this.handleUndoClick}><Icon icon={CloseIcon} /></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' onClick={this.handleFocalPointClick}><Icon icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
</div> </div>
{(media.get('description') || '').length === 0 && (
<div className='compose-form__upload__warning'> <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> <button type='button' className={classNames('icon-button', { active: missingDescription })} onClick={this.handleFocalPointClick}>{missingDescription && <Icon icon={WarningIcon} />} ALT</button>
</div> </div>
)}
</div> </div>
)} )}
</Motion> </Motion>

View file

@ -6,9 +6,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import AddPhotoAlternateIcon from '@/material-icons/400-24px/add_photo_alternate.svg?react'; import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
import { IconButton } from 'mastodon/components/icon_button';
import { IconButton } from '../../../components/icon_button';
const messages = defineMessages({ const messages = defineMessages({
upload: { id: 'upload_button.label', defaultMessage: 'Add images, a video or an audio file' }, upload: { id: 'upload_button.label', defaultMessage: 'Add images, a video or an audio file' },
@ -31,7 +30,6 @@ class UploadButton extends ImmutablePureComponent {
static propTypes = { static propTypes = {
disabled: PropTypes.bool, disabled: PropTypes.bool,
unavailable: PropTypes.bool,
onSelectFile: PropTypes.func.isRequired, onSelectFile: PropTypes.func.isRequired,
style: PropTypes.object, style: PropTypes.object,
resetFileKey: PropTypes.number, resetFileKey: PropTypes.number,
@ -54,17 +52,13 @@ class UploadButton extends ImmutablePureComponent {
}; };
render () { render () {
const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props; const { intl, resetFileKey, disabled, acceptContentTypes } = this.props;
if (unavailable) {
return null;
}
const message = intl.formatMessage(messages.upload); const message = intl.formatMessage(messages.upload);
return ( return (
<div className='compose-form__upload-button'> <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> <label>
<span style={{ display: 'none' }}>{message}</span> <span style={{ display: 'none' }}>{message}</span>
<input <input

View file

@ -1,7 +1,6 @@
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import SensitiveButtonContainer from '../containers/sensitive_button_container';
import UploadContainer from '../containers/upload_container'; import UploadContainer from '../containers/upload_container';
import UploadProgressContainer from '../containers/upload_progress_container'; import UploadProgressContainer from '../containers/upload_progress_container';
@ -15,17 +14,17 @@ export default class UploadForm extends ImmutablePureComponent {
const { mediaIds } = this.props; const { mediaIds } = this.props;
return ( return (
<div className='compose-form__upload-wrapper'> <>
<UploadProgressContainer /> <UploadProgressContainer />
<div className='compose-form__uploads-wrapper'> {mediaIds.size > 0 && (
<div className='compose-form__uploads'>
{mediaIds.map(id => ( {mediaIds.map(id => (
<UploadContainer id={id} key={id} /> <UploadContainer id={id} key={id} />
))} ))}
</div> </div>
)}
{!mediaIds.isEmpty() && <SensitiveButtonContainer />} </>
</div>
); );
} }

View file

@ -35,9 +35,7 @@ export default class UploadProgress extends PureComponent {
return ( return (
<div className='upload-progress'> <div className='upload-progress'>
<div className='upload-progress__icon'>
<Icon id='upload' icon={UploadFileIcon} /> <Icon id='upload' icon={UploadFileIcon} />
</div>
<div className='upload-progress__message'> <div className='upload-progress__message'>
{message} {message}

View file

@ -1,10 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { changeCircle } from '../../../actions/compose'; 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 => ({ const mapStateToProps = state => ({
unavailable: state.getIn(['compose', 'privacy']) !== 'circle' || !!state.getIn(['compose', 'id']), unavailable: state.getIn(['compose', 'privacy']) !== 'circle' || !!state.getIn(['compose', 'id']),
value: state.getIn(['compose', 'searchability']),
circles: state.get('circles'), circles: state.get('circles'),
circleId: state.getIn(['compose', 'circle_id']), circleId: state.getIn(['compose', 'circle_id']),
}); });
@ -15,6 +18,16 @@ const mapDispatchToProps = dispatch => ({
dispatch(changeCircle(circleId)); 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);

View file

@ -4,8 +4,7 @@ import { openModal, closeModal } from '../../../actions/modal';
import { isUserTouching } from '../../../is_mobile'; import { isUserTouching } from '../../../is_mobile';
import ExpirationDropdown from '../components/expiration_dropdown'; import ExpirationDropdown from '../components/expiration_dropdown';
const mapStateToProps = state => ({ const mapStateToProps = () => ({
value: state.getIn(['compose', 'privacy']),
}); });
const mapDispatchToProps = (dispatch, { onPickExpiration }) => ({ const mapDispatchToProps = (dispatch, { onPickExpiration }) => ({

View file

@ -2,8 +2,10 @@ import { injectIntl, defineMessages } from 'react-intl';
import { connect } from 'react-redux'; 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 { changeComposeMarkdown } from '../../../actions/compose';
import TextIconButton from '../components/text_icon_button';
const messages = defineMessages({ const messages = defineMessages({
marked: { id: 'compose_form.markdown.marked', defaultMessage: 'Markdown is enabled' }, marked: { id: 'compose_form.markdown.marked', defaultMessage: 'Markdown is enabled' },
@ -11,10 +13,12 @@ const messages = defineMessages({
}); });
const mapStateToProps = (state, { intl }) => ({ const mapStateToProps = (state, { intl }) => ({
label: 'MD', iconComponent: MarkdownIcon,
title: intl.formatMessage(state.getIn(['compose', 'markdown']) ? messages.marked : messages.unmarked), title: intl.formatMessage(state.getIn(['compose', 'markdown']) ? messages.marked : messages.unmarked),
active: state.getIn(['compose', 'markdown']), active: state.getIn(['compose', 'markdown']),
ariaControls: 'cw-markdown-input', ariaControls: 'cw-markdown-input',
size: 18,
inverted: true,
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
@ -25,4 +29,4 @@ const mapDispatchToProps = dispatch => ({
}); });
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TextIconButton)); export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(IconButton));

View file

@ -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));

View file

@ -4,7 +4,7 @@ import { addPoll, removePoll } from '../../../actions/compose';
import PollButton from '../components/poll_button'; import PollButton from '../components/poll_button';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
unavailable: false, disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0),
active: state.getIn(['compose', 'poll']) !== null, active: state.getIn(['compose', 'poll']) !== null,
}); });

View file

@ -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);

View file

@ -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);

View file

@ -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));

View file

@ -2,8 +2,10 @@ import { injectIntl, defineMessages } from 'react-intl';
import { connect } from 'react-redux'; 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 { changeComposeSpoilerness } from '../../../actions/compose';
import TextIconButton from '../components/text_icon_button';
const messages = defineMessages({ const messages = defineMessages({
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' }, marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' },
@ -11,10 +13,12 @@ const messages = defineMessages({
}); });
const mapStateToProps = (state, { intl }) => ({ const mapStateToProps = (state, { intl }) => ({
label: 'CW', iconComponent: WarningIcon,
title: intl.formatMessage(state.getIn(['compose', 'spoiler']) ? messages.marked : messages.unmarked), title: intl.formatMessage(state.getIn(['compose', 'spoiler']) ? messages.marked : messages.unmarked),
active: state.getIn(['compose', 'spoiler']), active: state.getIn(['compose', 'spoiler']),
ariaControls: 'cw-spoiler-input', ariaControls: 'cw-spoiler-input',
size: 18,
inverted: true,
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({
@ -25,4 +29,4 @@ const mapDispatchToProps = dispatch => ({
}); });
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TextIconButton)); export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(IconButton));

View file

@ -4,8 +4,7 @@ import { uploadCompose } from '../../../actions/compose';
import UploadButton from '../components/upload_button'; import UploadButton from '../components/upload_button';
const mapStateToProps = state => ({ 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')))), 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')))),
unavailable: false,
resetFileKey: state.getIn(['compose', 'resetFileKey']), resetFileKey: state.getIn(['compose', 'resetFileKey']),
}); });

View file

@ -5,6 +5,7 @@ import Upload from '../components/upload';
const mapStateToProps = (state, { id }) => ({ const mapStateToProps = (state, { id }) => ({
media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id),
sensitive: state.getIn(['compose', 'spoiler']),
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = dispatch => ({

View file

@ -30,7 +30,6 @@ import { isMobile } from '../../is_mobile';
import Motion from '../ui/util/optional_motion'; import Motion from '../ui/util/optional_motion';
import ComposeFormContainer from './containers/compose_form_container'; import ComposeFormContainer from './containers/compose_form_container';
import NavigationContainer from './containers/navigation_container';
import SearchContainer from './containers/search_container'; import SearchContainer from './containers/search_container';
import SearchResultsContainer from './containers/search_results_container'; import SearchResultsContainer from './containers/search_results_container';
@ -129,8 +128,6 @@ class Compose extends PureComponent {
<div className='drawer__pager'> <div className='drawer__pager'>
<div className='drawer__inner' onFocus={this.onFocus}> <div className='drawer__inner' onFocus={this.onFocus}>
<NavigationContainer onClose={this.onBlur} />
<ComposeFormContainer autoFocus={!isMobile(window.innerWidth)} /> <ComposeFormContainer autoFocus={!isMobile(window.innerWidth)} />
<div className='drawer__inner__mastodon'> <div className='drawer__inner__mastodon'>
@ -152,7 +149,6 @@ class Compose extends PureComponent {
return ( return (
<Column onFocus={this.onFocus}> <Column onFocus={this.onFocus}>
<NavigationContainer onClose={this.onBlur} />
<ComposeFormContainer /> <ComposeFormContainer />
<Helmet> <Helmet>

View file

@ -1,17 +1,24 @@
import PropTypes from 'prop-types'; 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 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 ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component'; import { useDispatch, useSelector } from 'react-redux';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.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 AttachmentList from 'mastodon/components/attachment_list';
import AvatarComposite from 'mastodon/components/avatar_composite'; import AvatarComposite from 'mastodon/components/avatar_composite';
import { IconButton } from 'mastodon/components/icon_button'; 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 StatusContent from 'mastodon/components/status_content';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import { autoPlayGif } from 'mastodon/initial_state'; import { autoPlayGif } from 'mastodon/initial_state';
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { makeGetStatus } from 'mastodon/selectors';
const messages = defineMessages({ const messages = defineMessages({
more: { id: 'status.more', defaultMessage: 'More' }, more: { id: 'status.more', defaultMessage: 'More' },
@ -29,25 +36,31 @@ const messages = defineMessages({
delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' }, delete: { id: 'conversation.delete', defaultMessage: 'Delete conversation' },
muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' },
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute 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 = { const getStatus = makeGetStatus();
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,
};
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) { if (autoPlayGif) {
return; return;
} }
@ -58,9 +71,9 @@ class Conversation extends ImmutablePureComponent {
let emoji = emojis[i]; let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-original'); emoji.src = emoji.getAttribute('data-original');
} }
}; }, []);
handleMouseLeave = ({ currentTarget }) => { const handleMouseLeave = useCallback(({ currentTarget }) => {
if (autoPlayGif) { if (autoPlayGif) {
return; return;
} }
@ -71,85 +84,107 @@ class Conversation extends ImmutablePureComponent {
let emoji = emojis[i]; let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-static'); emoji.src = emoji.getAttribute('data-static');
} }
}; }, []);
handleClick = () => {
if (!this.props.history) {
return;
}
const { lastStatus, unread, markRead } = this.props;
const handleClick = useCallback(() => {
if (unread) { 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 = () => { const handleMarkAsRead = useCallback(() => {
this.props.markRead(); dispatch(markConversationRead(id));
}; }, [dispatch, id]);
handleReply = () => { const handleReply = useCallback(() => {
this.props.reply(this.props.lastStatus, this.props.history); dispatch((_, getState) => {
}; let state = getState();
handleDelete = () => { if (state.getIn(['compose', 'text']).trim().length !== 0) {
this.props.delete(); 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 = () => { const handleDelete = useCallback(() => {
this.props.onMoveUp(this.props.conversationId); dispatch(deleteConversation(id));
}; }, [dispatch, id]);
handleHotkeyMoveDown = () => { const handleHotkeyMoveUp = useCallback(() => {
this.props.onMoveDown(this.props.conversationId); onMoveUp(id);
}; }, [id, onMoveUp]);
handleConversationMute = () => { const handleHotkeyMoveDown = useCallback(() => {
this.props.onMute(this.props.lastStatus); onMoveDown(id);
}; }, [id, onMoveDown]);
handleShowMore = () => { const handleConversationMute = useCallback(() => {
this.props.onToggleHidden(this.props.lastStatus); if (lastStatus.get('muted')) {
}; dispatch(unmuteStatus(lastStatus.get('id')));
} else {
dispatch(muteStatus(lastStatus.get('id')));
}
}, [dispatch, lastStatus]);
render () { const handleShowMore = useCallback(() => {
const { accounts, lastStatus, unread, scrollKey, intl } = this.props; if (lastStatus.get('hidden')) {
dispatch(revealStatus(lastStatus.get('id')));
} else {
dispatch(hideStatus(lastStatus.get('id')));
}
}, [dispatch, lastStatus]);
if (lastStatus === null) { if (!lastStatus) {
return null; return null;
} }
const menu = [ const menu = [
{ text: intl.formatMessage(messages.open), action: this.handleClick }, { text: intl.formatMessage(messages.open), action: handleClick },
null, null,
{ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: handleConversationMute },
]; ];
menu.push({ text: intl.formatMessage(lastStatus.get('muted') ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMute });
if (unread) { if (unread) {
menu.push({ text: intl.formatMessage(messages.markAsRead), action: this.handleMarkAsRead }); menu.push({ text: intl.formatMessage(messages.markAsRead), action: handleMarkAsRead });
menu.push(null); menu.push(null);
} }
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDelete }); menu.push({ text: intl.formatMessage(messages.delete), action: handleDelete });
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 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 handlers = { const handlers = {
reply: this.handleReply, reply: handleReply,
open: this.handleClick, open: handleClick,
moveUp: this.handleHotkeyMoveUp, moveUp: handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown, moveDown: handleHotkeyMoveDown,
toggleHidden: this.handleShowMore, toggleHidden: handleShowMore,
}; };
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex={0}> <div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex={0}>
<div className='conversation__avatar' onClick={this.handleClick} role='presentation'> <div className='conversation__avatar' onClick={handleClick} role='presentation'>
<AvatarComposite accounts={accounts} size={48} /> <AvatarComposite accounts={accounts} size={48} />
</div> </div>
@ -159,16 +194,16 @@ class Conversation extends ImmutablePureComponent {
{unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} /> {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
</div> </div>
<div className='conversation__content__names' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> <div className='conversation__content__names' onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} /> <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
</div> </div>
</div> </div>
<StatusContent <StatusContent
status={lastStatus} status={lastStatus}
onClick={this.handleClick} onClick={handleClick}
expanded={!lastStatus.get('hidden')} expanded={!lastStatus.get('hidden')}
onExpandedToggle={this.handleShowMore} onExpandedToggle={handleShowMore}
collapsible collapsible
/> />
@ -180,7 +215,7 @@ class Conversation extends ImmutablePureComponent {
)} )}
<div className='status__action-bar'> <div className='status__action-bar'>
<IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' iconComponent={ReplyIcon} onClick={this.handleReply} /> <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.reply)} icon='reply' iconComponent={ReplyIcon} onClick={handleReply} />
<div className='status__action-bar-dropdown'> <div className='status__action-bar-dropdown'>
<DropdownMenuContainer <DropdownMenuContainer
@ -199,8 +234,11 @@ class Conversation extends ImmutablePureComponent {
</div> </div>
</HotKeys> </HotKeys>
); );
} };
} Conversation.propTypes = {
conversation: ImmutablePropTypes.map.isRequired,
export default withRouter(injectIntl(Conversation)); scrollKey: PropTypes.string,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
};

View file

@ -1,77 +1,72 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useRef, useMemo, useCallback } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import { useSelector, useDispatch } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import ScrollableList from '../../../components/scrollable_list'; import { expandConversations } from 'mastodon/actions/conversations';
import ConversationContainer from '../containers/conversation_container'; import ScrollableList from 'mastodon/components/scrollable_list';
export default class ConversationsList extends ImmutablePureComponent { import { Conversation } from './conversation';
static propTypes = { const focusChild = (node, index, alignTop) => {
conversations: ImmutablePropTypes.list.isRequired, const element = node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
scrollKey: PropTypes.string.isRequired,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
onLoadMore: PropTypes.func,
};
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 (element) {
if (align_top && container.scrollTop > element.offsetTop) { if (alignTop && node.scrollTop > element.offsetTop) {
element.scrollIntoView(true); element.scrollIntoView(true);
} else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) { } else if (!alignTop && node.scrollTop + node.clientHeight < element.offsetTop + element.offsetHeight) {
element.scrollIntoView(false); element.scrollIntoView(false);
} }
element.focus(); element.focus();
} }
};
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');
const handleMoveUp = useCallback(id => {
const elementIndex = conversations.findIndex(x => x.get('id') === id) - 1;
focusChild(listRef.current.node, elementIndex, true);
}, [listRef, conversations]);
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);
} }
}, [debouncedLoadMore, lastStatusId]);
setRef = c => {
this.node = c;
};
handleLoadOlder = debounce(() => {
const last = this.props.conversations.last();
if (last && last.get('last_status')) {
this.props.onLoadMore(last.get('last_status'));
}
}, 300, { leading: true });
render () {
const { conversations, isLoading, onLoadMore, ...other } = this.props;
return ( return (
<ScrollableList {...other} isLoading={isLoading} showLoading={isLoading && conversations.isEmpty()} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}> <ScrollableList {...other} scrollKey={scrollKey} isLoading={isLoading} showLoading={isLoading && conversations.isEmpty()} hasMore={hasMore} onLoadMore={handleLoadMore} ref={listRef}>
{conversations.map(item => ( {conversations.map(item => (
<ConversationContainer <Conversation
key={item.get('id')} key={item.get('id')}
conversationId={item.get('id')} conversation={item}
onMoveUp={this.handleMoveUp} onMoveUp={handleMoveUp}
onMoveDown={this.handleMoveDown} onMoveDown={handleMoveDown}
scrollKey={this.props.scrollKey} scrollKey={scrollKey}
/> />
))} ))}
</ScrollableList> </ScrollableList>
); );
} };
} ConversationsList.propTypes = {
scrollKey: PropTypes.string.isRequired,
};

View file

@ -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));

View file

@ -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);

View file

@ -1,11 +1,11 @@
import PropTypes from 'prop-types'; 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 { 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 AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns'; import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
@ -14,93 +14,66 @@ import { connectDirectStream } from 'mastodon/actions/streaming';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import ConversationsListContainer from './containers/conversations_list_container'; import { ConversationsList } from './components/conversations_list';
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Private mentions' }, title: { id: 'column.direct', defaultMessage: 'Private mentions' },
}); });
class DirectTimeline extends PureComponent { const DirectTimeline = ({ columnId, multiColumn }) => {
const columnRef = useRef();
static propTypes = { const intl = useIntl();
dispatch: PropTypes.func.isRequired, const dispatch = useDispatch();
columnId: PropTypes.string, const pinned = !!columnId;
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
};
handlePin = () => {
const { columnId, dispatch } = this.props;
const handlePin = useCallback(() => {
if (columnId) { if (columnId) {
dispatch(removeColumn(columnId)); dispatch(removeColumn(columnId));
} else { } else {
dispatch(addColumn('DIRECT', {})); dispatch(addColumn('DIRECT', {}));
} }
}; }, [dispatch, columnId]);
handleMove = (dir) => { const handleMove = useCallback((dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir)); dispatch(moveColumn(columnId, dir));
}; }, [dispatch, columnId]);
handleHeaderClick = () => { const handleHeaderClick = useCallback(() => {
this.column.scrollTop(); columnRef.current.scrollTop();
}; }, [columnRef]);
componentDidMount () {
const { dispatch } = this.props;
useEffect(() => {
dispatch(mountConversations()); dispatch(mountConversations());
dispatch(expandConversations()); dispatch(expandConversations());
this.disconnect = dispatch(connectDirectStream());
}
componentWillUnmount () { const disconnect = dispatch(connectDirectStream());
this.props.dispatch(unmountConversations());
if (this.disconnect) { return () => {
this.disconnect(); dispatch(unmountConversations());
this.disconnect = null; disconnect();
}
}
setRef = c => {
this.column = c;
}; };
}, [dispatch]);
handleLoadMore = maxId => {
this.props.dispatch(expandConversations({ maxId }));
};
render () {
const { intl, hasUnread, columnId, multiColumn } = this.props;
const pinned = !!columnId;
return ( return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> <Column bindToDocument={!multiColumn} ref={columnRef} label={intl.formatMessage(messages.title)}>
<ColumnHeader <ColumnHeader
icon='at' icon='at'
iconComponent={AlternateEmailIcon} iconComponent={AlternateEmailIcon}
active={hasUnread}
title={intl.formatMessage(messages.title)} title={intl.formatMessage(messages.title)}
onPin={this.handlePin} onPin={handlePin}
onMove={this.handleMove} onMove={handleMove}
onClick={this.handleHeaderClick} onClick={handleHeaderClick}
pinned={pinned} pinned={pinned}
multiColumn={multiColumn} multiColumn={multiColumn}
/> />
<ConversationsListContainer <ConversationsList
trackScroll={!pinned} trackScroll={!pinned}
scrollKey={`direct_timeline-${columnId}`} scrollKey={`direct_timeline-${columnId}`}
timelineId='direct' 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} 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>} 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 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> <Helmet>
@ -109,8 +82,11 @@ class DirectTimeline extends PureComponent {
</Helmet> </Helmet>
</Column> </Column>
); );
} };
} DirectTimeline.propTypes = {
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
};
export default connect()(injectIntl(DirectTimeline)); export default DirectTimeline;

View file

@ -28,7 +28,7 @@ import ColumnHeader from 'mastodon/components/column_header';
import LinkFooter from 'mastodon/features/ui/components/link_footer'; import LinkFooter from 'mastodon/features/ui/components/link_footer';
import { me, showTrends } from '../../initial_state'; 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 ColumnLink from '../ui/components/column_link';
import ColumnSubheading from '../ui/components/column_subheading'; import ColumnSubheading from '../ui/components/column_subheading';
@ -149,7 +149,7 @@ class GettingStarted extends ImmutablePureComponent {
return ( return (
<Column> <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 scrollable scrollable--flex'>
<div className='getting-started__wrapper'> <div className='getting-started__wrapper'>

View file

@ -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'; const Compose = () => (
import LoadingBarContainer from '../../ui/containers/loading_bar_container'; <>
import ModalContainer from '../../ui/containers/modal_container'; <ComposeFormContainer autoFocus withoutNavigation />
import NotificationsContainer from '../../ui/containers/notifications_container';
export default class Compose extends PureComponent {
render () {
return (
<div>
<ComposeFormContainer autoFocus />
<NotificationsContainer /> <NotificationsContainer />
<ModalContainer /> <ModalContainer />
<LoadingBarContainer className='loading-bar' /> <LoadingBarContainer className='loading-bar' />
</div> </>
); );
}
} export default Compose;

View file

@ -6,7 +6,6 @@ import { connect } from 'react-redux';
import { changeComposing, mountCompose, unmountCompose } from 'mastodon/actions/compose'; import { changeComposing, mountCompose, unmountCompose } from 'mastodon/actions/compose';
import ServerBanner from 'mastodon/components/server_banner'; import ServerBanner from 'mastodon/components/server_banner';
import ComposeFormContainer from 'mastodon/features/compose/containers/compose_form_container'; 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 SearchContainer from 'mastodon/features/compose/containers/search_container';
import LinkFooter from './link_footer'; import LinkFooter from './link_footer';
@ -56,10 +55,7 @@ class ComposePanel extends PureComponent {
)} )}
{signedIn && ( {signedIn && (
<>
<NavigationContainer onClose={this.onBlur} />
<ComposeFormContainer singleColumn /> <ComposeFormContainer singleColumn />
</>
)} )}
<LinkFooter /> <LinkFooter />

View file

@ -21,7 +21,7 @@ import { Button } from 'mastodon/components/button';
import { GIFV } from 'mastodon/components/gifv'; import { GIFV } from 'mastodon/components/gifv';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
import Audio from 'mastodon/features/audio'; 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 UploadProgress from 'mastodon/features/compose/components/upload_progress';
import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components'; import { Tesseract as fetchTesseract } from 'mastodon/features/ui/util/async-components';
import { me } from 'mastodon/initial_state'; import { me } from 'mastodon/initial_state';

View file

@ -108,7 +108,6 @@ class MuteModal extends PureComponent {
<div> <div>
<span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span> <span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span>
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
<select value={muteDuration} onChange={this.changeMuteDuration}> <select value={muteDuration} onChange={this.changeMuteDuration}>
<option value={0}>{intl.formatMessage(messages.indefinite)}</option> <option value={0}>{intl.formatMessage(messages.indefinite)}</option>
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option> <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>

View file

@ -94,7 +94,6 @@ class NavigationPanel extends Component {
<div className='navigation-panel'> <div className='navigation-panel'>
<div className='navigation-panel__logo'> <div className='navigation-panel__logo'>
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link> <Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
{!banner && <hr />}
</div> </div>
{banner && {banner &&

View file

@ -3,6 +3,7 @@
"about.contact": "Kontak:", "about.contact": "Kontak:",
"about.disclaimer": "Mastodon is gratis oopbronsagteware en n handelsmerk van Mastodon gGmbH.", "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.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.silenced.title": "Beperk",
"about.domain_blocks.suspended.title": "Opgeskort", "about.domain_blocks.suspended.title": "Opgeskort",
"about.not_available": "Hierdie inligting is nie op hierdie bediener beskikbaar gestel nie.", "about.not_available": "Hierdie inligting is nie op hierdie bediener beskikbaar gestel nie.",

View file

@ -521,7 +521,7 @@
"poll.total_people": "{count, plural, one {# persona} other {# persones}}", "poll.total_people": "{count, plural, one {# persona} other {# persones}}",
"poll.total_votes": "{count, plural, one {# vot} other {# vots}}", "poll.total_votes": "{count, plural, one {# vot} other {# vots}}",
"poll.vote": "Vota", "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.votes": "{votes, plural, one {# vot} other {# vots}}",
"poll_button.add_poll": "Afegeix una enquesta", "poll_button.add_poll": "Afegeix una enquesta",
"poll_button.remove_poll": "Elimina l'enquesta", "poll_button.remove_poll": "Elimina l'enquesta",

View file

@ -96,7 +96,6 @@
"announcement.announcement": "Announcement", "announcement.announcement": "Announcement",
"attachments_list.unprocessed": "(unprocessed)", "attachments_list.unprocessed": "(unprocessed)",
"audio.hide": "Hide audio", "audio.hide": "Hide audio",
"autosuggest_hashtag.per_week": "{count} per week",
"boost_modal.combo": "You can press {combo} to skip this next time", "boost_modal.combo": "You can press {combo} to skip this next time",
"bundle_column_error.copy_stacktrace": "Copy error report", "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.", "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.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.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.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 option",
"compose_form.poll.add_option": "Add a choice",
"compose_form.poll.duration": "Poll duration", "compose_form.poll.duration": "Poll duration",
"compose_form.poll.option_placeholder": "Choice {number}", "compose_form.poll.multiple": "Multiple choice",
"compose_form.poll.remove_option": "Remove this 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_multiple": "Change poll to allow multiple choices",
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice", "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_form": "New post",
"compose_form.publish_loud": "{publish}!", "compose_form.reply": "Reply",
"compose_form.save_changes": "Save changes", "compose_form.save_changes": "Update",
"compose_form.sensitive.hide": "{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}", "compose_form.searchability_warning": "Self only searchability is not available other mastodon servers. Others can search your post.",
"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.spoiler.marked": "Remove content warning", "compose_form.spoiler.marked": "Remove content warning",
"compose_form.spoiler.unmarked": "Add 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", "confirmation_modal.cancel": "Cancel",
"confirmations.block.block_and_report": "Block & Report", "confirmations.block.block_and_report": "Block & Report",
"confirmations.block.confirm": "Block", "confirmations.block.confirm": "Block",
@ -428,7 +427,6 @@
"navigation_bar.direct": "Private mentions", "navigation_bar.direct": "Private mentions",
"navigation_bar.discover": "Discover", "navigation_bar.discover": "Discover",
"navigation_bar.domain_blocks": "Blocked domains", "navigation_bar.domain_blocks": "Blocked domains",
"navigation_bar.edit_profile": "Edit profile",
"navigation_bar.explore": "Explore", "navigation_bar.explore": "Explore",
"navigation_bar.favourites": "Favorites", "navigation_bar.favourites": "Favorites",
"navigation_bar.filters": "Muted words", "navigation_bar.filters": "Muted words",
@ -551,22 +549,23 @@
"poll_button.add_poll": "Add a poll", "poll_button.add_poll": "Add a poll",
"poll_button.remove_poll": "Remove poll", "poll_button.remove_poll": "Remove poll",
"privacy.change": "Change post privacy", "privacy.change": "Change post privacy",
"privacy.direct.long": "Visible for mentioned users only", "privacy.direct.long": "Everyone mentioned in the post",
"privacy.direct.short": "Mentioned people only", "privacy.direct.short": "Specific people",
"privacy.limited.short": "Limited", "privacy.limited.short": "Limited",
"privacy.login.long": "Visible for login users only", "privacy.login.long": "Visible for login users only",
"privacy.login.short": "Login only", "privacy.login.short": "Login only",
"privacy.mutual.long": "Mutual followers only", "privacy.mutual.long": "Mutual followers only",
"privacy.mutual.short": "Mutual only", "privacy.mutual.short": "Mutual only",
"privacy.personal.short": "Yourself only", "privacy.personal.short": "Yourself only",
"privacy.private.long": "Visible for followers only", "privacy.private.long": "Only your followers",
"privacy.private.short": "Followers only", "privacy.private.short": "Followers",
"privacy.public.long": "Visible for all", "privacy.public.long": "Anyone on and off Mastodon",
"privacy.public.short": "Public", "privacy.public.short": "Public",
"privacy.public_unlisted.long": "Visible for all without GTL", "privacy.public_unlisted.long": "Visible for all without GTL",
"privacy.public_unlisted.short": "Public unlisted", "privacy.public_unlisted.short": "Public unlisted",
"privacy.unlisted.long": "Visible for all, but opted-out of discovery features", "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.short": "Unlisted", "privacy.unlisted.long": "Fewer algorithmic fanfares",
"privacy.unlisted.short": "Quiet public",
"privacy_policy.last_updated": "Last updated {date}", "privacy_policy.last_updated": "Last updated {date}",
"privacy_policy.title": "Privacy Policy", "privacy_policy.title": "Privacy Policy",
"reaction_deck.add": "Add", "reaction_deck.add": "Add",
@ -586,7 +585,9 @@
"relative_time.minutes": "{number}m", "relative_time.minutes": "{number}m",
"relative_time.seconds": "{number}s", "relative_time.seconds": "{number}s",
"relative_time.today": "today", "relative_time.today": "today",
"reply_indicator.attachments": "{count, plural, one {# attachment} other {# attachments}}",
"reply_indicator.cancel": "Cancel", "reply_indicator.cancel": "Cancel",
"reply_indicator.poll": "Poll",
"report.block": "Block", "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.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", "report.categories.legal": "Legal",
@ -768,10 +769,8 @@
"upload_error.poll": "File upload not allowed with polls.", "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.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": "Describe for people who are blind or have low vision",
"upload_form.description_missing": "No description added",
"upload_form.edit": "Edit", "upload_form.edit": "Edit",
"upload_form.thumbnail": "Change thumbnail", "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_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.analyzing_picture": "Analyzing picture…",
"upload_modal.apply": "Apply", "upload_modal.apply": "Apply",

View file

@ -216,7 +216,7 @@
"compose_form.direct_message_warning_learn_more": "もっと詳しく", "compose_form.direct_message_warning_learn_more": "もっと詳しく",
"compose_form.encryption_warning": "Mastodonの投稿はエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。", "compose_form.encryption_warning": "Mastodonの投稿はエンドツーエンド暗号化に対応していません。安全に送受信されるべき情報をMastodonで共有しないでください。",
"compose_form.hashtag_warning": "この投稿は公開設定ではないのでハッシュタグの一覧に表示されません。公開投稿だけがハッシュタグで検索できます。", "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": "あなたのアカウントは{locked}になっていません。誰でもあなたをフォローすることができ、フォロワー限定の投稿を見ることができます。",
"compose_form.lock_disclaimer.lock": "承認制", "compose_form.lock_disclaimer.lock": "承認制",
"compose_form.markdown.marked": "Markdown有効", "compose_form.markdown.marked": "Markdown有効",
@ -634,15 +634,15 @@
"poll_button.add_poll": "アンケートを追加", "poll_button.add_poll": "アンケートを追加",
"poll_button.remove_poll": "アンケートを削除", "poll_button.remove_poll": "アンケートを削除",
"privacy.change": "公開範囲を変更", "privacy.change": "公開範囲を変更",
"privacy.circle.long": "サークルメンバーのみ", "privacy.circle.long": "サークルメンバーのみ閲覧可",
"privacy.circle.short": "サークル", "privacy.circle.short": "サークル (投稿時点)",
"privacy.direct.long": "指定された相手のみ閲覧可", "privacy.direct.long": "指定された相手のみ閲覧可",
"privacy.direct.short": "指定された相手のみ", "privacy.direct.short": "指定された相手のみ",
"privacy.limited.short": "限定投稿", "privacy.limited.short": "限定投稿",
"privacy.login.long": "ログインユーザーのみ閲覧可、公開", "privacy.login.long": "ログインユーザーのみ閲覧可、公開",
"privacy.login.short": "ログインユーザーのみ", "privacy.login.short": "ログインユーザーのみ",
"privacy.mutual.long": "相互フォローさんのみ閲覧可、限定投稿", "privacy.mutual.long": "相互フォローのみ閲覧可",
"privacy.mutual.short": "相互のみ", "privacy.mutual.short": "相互 (投稿時点)",
"privacy.personal.short": "自分限定", "privacy.personal.short": "自分限定",
"privacy.private.long": "フォロワーのみ閲覧可", "privacy.private.long": "フォロワーのみ閲覧可",
"privacy.private.short": "フォロワーのみ", "privacy.private.short": "フォロワーのみ",
@ -650,6 +650,8 @@
"privacy.public.short": "公開", "privacy.public.short": "公開",
"privacy.public_unlisted.long": "誰でも閲覧可、ホームローカルTL", "privacy.public_unlisted.long": "誰でも閲覧可、ホームローカルTL",
"privacy.public_unlisted.short": "ローカル公開", "privacy.public_unlisted.short": "ローカル公開",
"privacy.reply.long": "元投稿と同じメンバーが閲覧可",
"privacy.reply.short": "限定投稿への返信",
"privacy.unlisted.long": "誰でも閲覧可、ホームTL", "privacy.unlisted.long": "誰でも閲覧可、ホームTL",
"privacy.unlisted.short": "非収載", "privacy.unlisted.short": "非収載",
"privacy_policy.last_updated": "{date}に更新", "privacy_policy.last_updated": "{date}に更新",

View file

@ -18,6 +18,7 @@
"account.blocked": "Blocat", "account.blocked": "Blocat",
"account.browse_more_on_origin_server": "Navigar sul perfil original", "account.browse_more_on_origin_server": "Navigar sul perfil original",
"account.cancel_follow_request": "Retirar la demanda dabonament", "account.cancel_follow_request": "Retirar la demanda dabonament",
"account.copy": "Copiar lo ligam del perfil",
"account.direct": "Mencionar @{name} en privat", "account.direct": "Mencionar @{name} en privat",
"account.disable_notifications": "Quitar de mavisar quand @{name} publica quicòm", "account.disable_notifications": "Quitar de mavisar quand @{name} publica quicòm",
"account.domain_blocked": "Domeni amagat", "account.domain_blocked": "Domeni amagat",
@ -28,6 +29,7 @@
"account.featured_tags.last_status_never": "Cap de publicacion", "account.featured_tags.last_status_never": "Cap de publicacion",
"account.featured_tags.title": "Etiquetas en avant de {name}", "account.featured_tags.title": "Etiquetas en avant de {name}",
"account.follow": "Sègre", "account.follow": "Sègre",
"account.follow_back": "Sègre en retorn",
"account.followers": "Seguidors", "account.followers": "Seguidors",
"account.followers.empty": "Degun sèc pas aqueste utilizaire pel moment.", "account.followers.empty": "Degun sèc pas aqueste utilizaire pel moment.",
"account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidors}}", "account.followers_counter": "{count, plural, one {{counter} Seguidor} other {{counter} Seguidors}}",
@ -48,6 +50,7 @@
"account.mute_notifications_short": "Amudir las notificacions", "account.mute_notifications_short": "Amudir las notificacions",
"account.mute_short": "Amudir", "account.mute_short": "Amudir",
"account.muted": "Mes en silenci", "account.muted": "Mes en silenci",
"account.mutual": "Mutual",
"account.no_bio": "Cap de descripcion pas fornida.", "account.no_bio": "Cap de descripcion pas fornida.",
"account.open_original_page": "Dobrir la pagina dorigina", "account.open_original_page": "Dobrir la pagina dorigina",
"account.posts": "Tuts", "account.posts": "Tuts",
@ -172,6 +175,7 @@
"conversation.mark_as_read": "Marcar coma legida", "conversation.mark_as_read": "Marcar coma legida",
"conversation.open": "Veire la conversacion", "conversation.open": "Veire la conversacion",
"conversation.with": "Amb {names}", "conversation.with": "Amb {names}",
"copy_icon_button.copied": "Copiat al quichapapièr",
"copypaste.copied": "Copiat", "copypaste.copied": "Copiat",
"copypaste.copy_to_clipboard": "Copiar al quichapapièr", "copypaste.copy_to_clipboard": "Copiar al quichapapièr",
"directory.federated": "Del fediverse conegut", "directory.federated": "Del fediverse conegut",
@ -294,6 +298,8 @@
"keyboard_shortcuts.direct": "to open direct messages column", "keyboard_shortcuts.direct": "to open direct messages column",
"keyboard_shortcuts.down": "far davalar dins la lista", "keyboard_shortcuts.down": "far davalar dins la lista",
"keyboard_shortcuts.enter": "dobrir los estatuts", "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.federated": "dobrir lo flux public global",
"keyboard_shortcuts.heading": "Acorchis clavièr", "keyboard_shortcuts.heading": "Acorchis clavièr",
"keyboard_shortcuts.home": "dobrir lo flux public local", "keyboard_shortcuts.home": "dobrir lo flux public local",
@ -339,6 +345,7 @@
"lists.search": "Cercar demest lo mond que seguètz", "lists.search": "Cercar demest lo mond que seguètz",
"lists.subheading": "Vòstras listas", "lists.subheading": "Vòstras listas",
"load_pending": "{count, plural, one {# nòu element} other {# nòu elements}}", "load_pending": "{count, plural, one {# nòu element} other {# nòu elements}}",
"loading_indicator.label": "Cargament…",
"media_gallery.toggle_visible": "Modificar la visibilitat", "media_gallery.toggle_visible": "Modificar la visibilitat",
"mute_modal.duration": "Durada", "mute_modal.duration": "Durada",
"mute_modal.hide_notifications": "Rescondre las notificacions daquesta persona?", "mute_modal.hide_notifications": "Rescondre las notificacions daquesta persona?",
@ -371,6 +378,7 @@
"not_signed_in_indicator.not_signed_in": "Devètz vos connectar per accedir a aquesta ressorsa.", "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.report": "{name} senhalèt {target}",
"notification.admin.sign_up": "{name} se marquèt", "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": "{name} vos sèc",
"notification.follow_request": "{name} a demandat a vos sègre", "notification.follow_request": "{name} a demandat a vos sègre",
"notification.mention": "{name} vos a mencionat", "notification.mention": "{name} vos a mencionat",
@ -423,6 +431,8 @@
"onboarding.compose.template": "Adiu #Mastodon !", "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.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.follows.title": "Popular on Mastodon",
"onboarding.profile.display_name": "Nom dafichatge",
"onboarding.profile.note": "Biografia",
"onboarding.share.title": "Partejar vòstre perfil", "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.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?", "onboarding.start.skip": "Want to skip right ahead?",
@ -504,6 +514,7 @@
"report_notification.categories.spam": "Messatge indesirable", "report_notification.categories.spam": "Messatge indesirable",
"report_notification.categories.violation": "Violacion de las règlas", "report_notification.categories.violation": "Violacion de las règlas",
"report_notification.open": "Dobrir lo senhalament", "report_notification.open": "Dobrir lo senhalament",
"search.no_recent_searches": "Cap de recèrcas recentas",
"search.placeholder": "Recercar", "search.placeholder": "Recercar",
"search.search_or_paste": "Recercar o picar una URL", "search.search_or_paste": "Recercar o picar una URL",
"search_popout.language_code": "Còdi ISO de lenga", "search_popout.language_code": "Còdi ISO de lenga",
@ -536,6 +547,7 @@
"status.copy": "Copiar lo ligam de lestatut", "status.copy": "Copiar lo ligam de lestatut",
"status.delete": "Escafar", "status.delete": "Escafar",
"status.detailed_status": "Vista detalhada de la convèrsa", "status.detailed_status": "Vista detalhada de la convèrsa",
"status.direct": "Mencionar @{name} en privat",
"status.direct_indicator": "Mencion privada", "status.direct_indicator": "Mencion privada",
"status.edit": "Modificar", "status.edit": "Modificar",
"status.edited": "Modificat {date}", "status.edited": "Modificat {date}",
@ -626,6 +638,7 @@
"upload_modal.preview_label": "Apercebut ({ratio})", "upload_modal.preview_label": "Apercebut ({ratio})",
"upload_progress.label": "Mandadís…", "upload_progress.label": "Mandadís…",
"upload_progress.processing": "Tractament…", "upload_progress.processing": "Tractament…",
"username.taken": "Aqueste nom dutilizaire es pres. Ensajatz-ne un autre",
"video.close": "Tampar la vidèo", "video.close": "Tampar la vidèo",
"video.download": "Telecargar lo fichièr", "video.download": "Telecargar lo fichièr",
"video.exit_fullscreen": "Sortir plen ecran", "video.exit_fullscreen": "Sortir plen ecran",

View file

@ -48,7 +48,7 @@
"account.locked_info": "此帳號的隱私狀態設定為鎖定。該擁有者會手動審核能跟隨此帳號的人。", "account.locked_info": "此帳號的隱私狀態設定為鎖定。該擁有者會手動審核能跟隨此帳號的人。",
"account.media": "媒體", "account.media": "媒體",
"account.mention": "提及 @{name}", "account.mention": "提及 @{name}",
"account.moved_to": "{name} 現在的新帳號為:", "account.moved_to": "{name} 目前的新帳號為:",
"account.mute": "靜音 @{name}", "account.mute": "靜音 @{name}",
"account.mute_notifications_short": "靜音推播通知", "account.mute_notifications_short": "靜音推播通知",
"account.mute_short": "靜音", "account.mute_short": "靜音",
@ -59,7 +59,7 @@
"account.posts": "嘟文", "account.posts": "嘟文",
"account.posts_with_replies": "嘟文與回覆", "account.posts_with_replies": "嘟文與回覆",
"account.report": "檢舉 @{name}", "account.report": "檢舉 @{name}",
"account.requested": "正在等待核准。按一下以取消跟隨請求", "account.requested": "正在等候審核。按一下以取消跟隨請求",
"account.requested_follow": "{name} 要求跟隨您", "account.requested_follow": "{name} 要求跟隨您",
"account.share": "分享 @{name} 的個人檔案", "account.share": "分享 @{name} 的個人檔案",
"account.show_reblogs": "顯示來自 @{name} 的嘟文", "account.show_reblogs": "顯示來自 @{name} 的嘟文",
@ -84,7 +84,7 @@
"admin.impact_report.title": "影響總結", "admin.impact_report.title": "影響總結",
"alert.rate_limited.message": "請於 {retry_time, time, medium} 後重試。", "alert.rate_limited.message": "請於 {retry_time, time, medium} 後重試。",
"alert.rate_limited.title": "已限速", "alert.rate_limited.title": "已限速",
"alert.unexpected.message": "發生非預期的錯誤。", "alert.unexpected.message": "發生非預期的錯誤。",
"alert.unexpected.title": "哎呀!", "alert.unexpected.title": "哎呀!",
"announcement.announcement": "公告", "announcement.announcement": "公告",
"attachments_list.unprocessed": "(未經處理)", "attachments_list.unprocessed": "(未經處理)",
@ -241,7 +241,7 @@
"empty_column.followed_tags": "您還沒有跟隨任何主題標籤。當您跟隨主題標籤時,它們將於此顯示。", "empty_column.followed_tags": "您還沒有跟隨任何主題標籤。當您跟隨主題標籤時,它們將於此顯示。",
"empty_column.hashtag": "這個主題標籤下什麼也沒有。", "empty_column.hashtag": "這個主題標籤下什麼也沒有。",
"empty_column.home": "您的首頁時間軸是空的!跟隨更多人來將它填滿吧!", "empty_column.home": "您的首頁時間軸是空的!跟隨更多人來將它填滿吧!",
"empty_column.list": "這份列表下什麼也沒有。當此列表的成員嘟出新的嘟文時,它們將顯示於此。", "empty_column.list": "這份列表下什麼也沒有。當此列表的成員嘟出新的嘟文時,它們將顯示於此。",
"empty_column.lists": "您還沒有建立任何列表。當您建立列表時,它將於此顯示。", "empty_column.lists": "您還沒有建立任何列表。當您建立列表時,它將於此顯示。",
"empty_column.mutes": "您尚未靜音任何使用者。", "empty_column.mutes": "您尚未靜音任何使用者。",
"empty_column.notifications": "您還沒有收到任何通知,當您與別人開始互動時,它將於此顯示。", "empty_column.notifications": "您還沒有收到任何通知,當您與別人開始互動時,它將於此顯示。",
@ -303,8 +303,8 @@
"hashtag.counter_by_accounts": "{count, plural, one {{counter} 名} other {{counter} 名}}參與者", "hashtag.counter_by_accounts": "{count, plural, one {{counter} 名} other {{counter} 名}}參與者",
"hashtag.counter_by_uses": "{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.counter_by_uses_today": "本日有 {count, plural, one {{counter} 則} other {{counter} 則}}嘟文",
"hashtag.follow": "追蹤主題標籤", "hashtag.follow": "跟隨主題標籤",
"hashtag.unfollow": "取消追蹤主題標籤", "hashtag.unfollow": "取消跟隨主題標籤",
"hashtags.and_other": "…及其他 {count, plural, other {# 個}}", "hashtags.and_other": "…及其他 {count, plural, other {# 個}}",
"home.actions.go_to_explore": "看看發生什麼新鮮事", "home.actions.go_to_explore": "看看發生什麼新鮮事",
"home.actions.go_to_suggestions": "尋找一些人來跟隨", "home.actions.go_to_suggestions": "尋找一些人來跟隨",

View file

@ -43,9 +43,7 @@ import {
COMPOSE_RESET, COMPOSE_RESET,
COMPOSE_POLL_ADD, COMPOSE_POLL_ADD,
COMPOSE_POLL_REMOVE, COMPOSE_POLL_REMOVE,
COMPOSE_POLL_OPTION_ADD,
COMPOSE_POLL_OPTION_CHANGE, COMPOSE_POLL_OPTION_CHANGE,
COMPOSE_POLL_OPTION_REMOVE,
COMPOSE_POLL_SETTINGS_CHANGE, COMPOSE_POLL_SETTINGS_CHANGE,
COMPOSE_CIRCLE_CHANGE, COMPOSE_CIRCLE_CHANGE,
INIT_MEDIA_EDIT_MODAL, 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) { export default function compose(state = initialState, action) {
switch(action.type) { switch(action.type) {
case STORE_HYDRATE: case STORE_HYDRATE:
@ -635,12 +645,8 @@ export default function compose(state = initialState, action) {
return state.set('poll', initialPoll); return state.set('poll', initialPoll);
case COMPOSE_POLL_REMOVE: case COMPOSE_POLL_REMOVE:
return state.set('poll', null); 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: case COMPOSE_POLL_OPTION_CHANGE:
return state.setIn(['poll', 'options', action.index], action.title); return updatePoll(state, action.index, action.title);
case COMPOSE_POLL_OPTION_REMOVE:
return state.updateIn(['poll', 'options'], options => options.delete(action.index));
case COMPOSE_POLL_SETTINGS_CHANGE: case COMPOSE_POLL_SETTINGS_CHANGE:
return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple)); return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple));
case COMPOSE_CIRCLE_CHANGE: case COMPOSE_CIRCLE_CHANGE:

View file

@ -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

View file

@ -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

View 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

View 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

View 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-240H320ZM160-80q-33 0-56.5-23.5T80-160v-560h80v560h560v80H160Z"/></svg>

After

Width:  |  Height:  |  Size: 322 B

View 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

View 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-40Z"/></svg>

After

Width:  |  Height:  |  Size: 259 B

View 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

View 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

View 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

View 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

View 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

View file

@ -13,10 +13,12 @@ function loaded() {
if (mountNode) { if (mountNode) {
const attr = mountNode.getAttribute('data-props'); const attr = mountNode.getAttribute('data-props');
if(!attr) return;
if (!attr) return;
const props = JSON.parse(attr); const props = JSON.parse(attr);
const root = createRoot(mountNode); const root = createRoot(mountNode);
root.render(<ComposeContainer {...props} />); root.render(<ComposeContainer {...props} />);
} }
} }

View file

@ -1,20 +1,7 @@
.compose-form {
.compose-form__modifiers {
.compose-form__upload {
&-description {
input {
&::placeholder {
opacity: 1;
}
}
}
}
}
}
.status__content a, .status__content a,
.link-footer a,
.reply-indicator__content a, .reply-indicator__content a,
.edit-indicator__content a,
.link-footer a,
.status__content__read-more-button, .status__content__read-more-button,
.status__content__translate-button { .status__content__translate-button {
text-decoration: underline; 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; color: $highlight-text-color;
} }
@ -50,24 +39,10 @@
color: $darker-text-color; color: $darker-text-color;
} }
.compose-form__poll-wrapper .button.button-secondary, .report-dialog-modal__textarea::placeholder {
.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 {
color: $inverted-text-color; 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 { .link-button:disabled {
cursor: not-allowed; cursor: not-allowed;

View file

@ -33,9 +33,6 @@ textarea {
} }
.compose-form .compose-form__warning, .compose-form .compose-form__warning,
.reply-indicator__content,
.reply-indicator__display-name,
.reply-indicator__cancel,
.autosuggest-textarea__suggestions__item { .autosuggest-textarea__suggestions__item {
color: $ui-base-color; color: $ui-base-color;
} }

View file

@ -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, .search__input,
.setting-text, .setting-text,
.report-dialog-modal__textarea, .report-dialog-modal__textarea,
@ -172,28 +168,11 @@ html {
border-bottom: 0; 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 { .notification__filter-bar {
border: 1px solid lighten($ui-base-color, 8%); border: 1px solid lighten($ui-base-color, 8%);
border-top: 0; 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__header,
.drawer__inner { .drawer__inner {
background: $white; background: $white;
@ -206,52 +185,6 @@ html {
no-repeat bottom / 100% auto; 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 { .upload-progress__backdrop {
background: $ui-base-color; background: $ui-base-color;
} }
@ -283,55 +216,11 @@ html {
background: $ui-base-color; 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 { .account-gallery__item a {
background-color: $ui-base-color; 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 // 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,
.actions-modal ul li:not(:empty) a.active button, .actions-modal ul li:not(:empty) a.active button,
.actions-modal ul li:not(:empty) a:active, .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:focus button,
.actions-modal ul li:not(:empty) a:hover, .actions-modal ul li:not(:empty) a:hover,
.actions-modal ul li:not(:empty) a:hover button, .actions-modal ul li:not(:empty) a:hover button,
.language-dropdown__dropdown__results__item.active,
.admin-wrapper .sidebar ul .simple-navigation-active-leaf a, .admin-wrapper .sidebar ul .simple-navigation-active-leaf a,
.simple_form .block-button, .simple_form .block-button,
.simple_form .button, .simple_form .button,
@ -348,19 +236,6 @@ html {
color: $white; 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, .compare-history-modal .report-modal__target,
.report-dialog-modal .poll__option.dialog-option { .report-dialog-modal .poll__option.dialog-option {
border-bottom-color: lighten($ui-base-color, 4%); border-bottom-color: lighten($ui-base-color, 4%);
@ -394,10 +269,7 @@ html {
.reactions-bar__item:hover, .reactions-bar__item:hover,
.reactions-bar__item:focus, .reactions-bar__item:focus,
.reactions-bar__item:active, .reactions-bar__item:active {
.language-dropdown__dropdown__results__item:hover,
.language-dropdown__dropdown__results__item:focus,
.language-dropdown__dropdown__results__item:active {
background-color: $ui-base-color; background-color: $ui-base-color;
} }
@ -640,11 +512,6 @@ html {
} }
} }
.reply-indicator {
background: transparent;
border: 1px solid lighten($ui-base-color, 8%);
}
.status__content, .status__content,
.reply-indicator__content { .reply-indicator__content {
a { a {
@ -684,3 +551,30 @@ html {
background-color: rgba($ui-highlight-color, 0.15); 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%);
}

View file

@ -5,7 +5,7 @@ $white: #ffffff;
$classic-base-color: #282c37; $classic-base-color: #282c37;
$classic-primary-color: #9baec8; $classic-primary-color: #9baec8;
$classic-secondary-color: #d9e1e8; $classic-secondary-color: #d9e1e8;
$classic-highlight-color: #858afa; $classic-highlight-color: #6364ff;
$blurple-600: #563acc; // Iris $blurple-600: #563acc; // Iris
$blurple-500: #6364ff; // Brand purple $blurple-500: #6364ff; // Brand purple
@ -37,7 +37,7 @@ $ui-button-tertiary-border-color: $blurple-500 !default;
$primary-text-color: $black !default; $primary-text-color: $black !default;
$darker-text-color: $classic-base-color !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; $dark-text-color: #444b5d;
$action-button-color: #606984; $action-button-color: #606984;
@ -58,3 +58,8 @@ $account-background-color: $white !default;
} }
$emojis-requiring-inversion: 'chains'; $emojis-requiring-inversion: 'chains';
.theme-mastodon-light {
--dropdown-border-color: #d9e1e8;
--dropdown-background-color: #fff;
}

View file

@ -15,13 +15,14 @@
outline: 0; outline: 0;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
border: 0;
box-shadow: none; box-shadow: none;
font-family: inherit; font-family: inherit;
background: $ui-base-color; background: $ui-base-color;
color: $darker-text-color; color: $darker-text-color;
border-radius: 4px; border-radius: 4px;
font-size: 14px; border: 1px solid lighten($ui-base-color, 8%);
font-size: 17px;
line-height: normal;
margin: 0; margin: 0;
} }

View file

@ -1321,6 +1321,9 @@ a.sparkline {
&__label { &__label {
padding: 15px; padding: 15px;
display: flex;
gap: 8px;
align-items: center;
} }
&__rules { &__rules {
@ -1331,6 +1334,9 @@ a.sparkline {
&__rule { &__rule {
cursor: pointer; cursor: pointer;
padding: 15px; padding: 15px;
display: flex;
gap: 8px;
align-items: center;
} }
} }

View file

@ -8,7 +8,7 @@
body { body {
font-family: $font-sans-serif, sans-serif; font-family: $font-sans-serif, sans-serif;
background: darken($ui-base-color, 7%); background: darken($ui-base-color, 8%);
font-size: 13px; font-size: 13px;
line-height: 18px; line-height: 18px;
font-weight: 400; font-weight: 400;

File diff suppressed because it is too large Load diff

View file

@ -40,13 +40,12 @@
.compose-form { .compose-form {
width: 400px; width: 400px;
margin: 0 auto; margin: 0 auto;
padding: 20px 0; padding: 10px 0;
margin-top: 40px; padding-bottom: 20px;
box-sizing: border-box; box-sizing: border-box;
@media screen and (width <= 400px) { @media screen and (width <= 400px) {
width: 100%; width: 100%;
margin-top: 0;
padding: 20px; padding: 20px;
} }
} }
@ -56,13 +55,15 @@
width: 400px; width: 400px;
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
font-size: 13px; align-items: center;
line-height: 18px; gap: 10px;
font-size: 14px;
line-height: 20px;
box-sizing: border-box; box-sizing: border-box;
padding: 20px 0; padding: 20px 0;
margin-top: 40px; margin-top: 40px;
margin-bottom: 10px; margin-bottom: 10px;
border-bottom: 1px solid $ui-base-color; border-bottom: 1px solid lighten($ui-base-color, 8%);
@media screen and (width <= 440px) { @media screen and (width <= 440px) {
width: 100%; width: 100%;
@ -71,9 +72,9 @@
} }
.avatar { .avatar {
width: 40px; width: 48px;
height: 40px; height: 48px;
margin-inline-end: 10px; flex: 0 0 auto;
img { img {
width: 100%; width: 100%;
@ -87,13 +88,14 @@
.name { .name {
flex: 1 1 auto; flex: 1 1 auto;
color: $secondary-text-color; color: $secondary-text-color;
width: calc(100% - 90px);
.username { .username {
display: block; display: block;
font-weight: 500; font-size: 16px;
line-height: 24px;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
color: $primary-text-color;
} }
} }
@ -101,7 +103,7 @@
display: block; display: block;
font-size: 32px; font-size: 32px;
line-height: 40px; line-height: 40px;
margin-inline-start: 10px; flex: 0 0 auto;
} }
} }

View file

@ -1,7 +1,6 @@
.emoji-mart { .emoji-mart {
font-size: 13px; font-size: 13px;
display: inline-block; display: inline-block;
color: $inverted-text-color;
&, &,
* { * {
@ -15,13 +14,13 @@
} }
.emoji-mart-bar { .emoji-mart-bar {
border: 0 solid darken($ui-secondary-color, 8%); border: 0 solid var(--dropdown-border-color);
&:first-child { &:first-child {
border-bottom-width: 1px; border-bottom-width: 1px;
border-top-left-radius: 5px; border-top-left-radius: 5px;
border-top-right-radius: 5px; border-top-right-radius: 5px;
background: $ui-secondary-color; background: var(--dropdown-border-color);
} }
&:last-child { &:last-child {
@ -36,7 +35,6 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 0 6px; padding: 0 6px;
color: $lighter-text-color;
line-height: 0; line-height: 0;
} }
@ -50,9 +48,10 @@
cursor: pointer; cursor: pointer;
background: transparent; background: transparent;
border: 0; border: 0;
color: $darker-text-color;
&:hover { &:hover {
color: darken($lighter-text-color, 4%); color: lighten($darker-text-color, 4%);
} }
} }
@ -60,7 +59,7 @@
color: $highlight-text-color; color: $highlight-text-color;
&:hover { &:hover {
color: darken($highlight-text-color, 4%); color: lighten($highlight-text-color, 4%);
} }
.emoji-mart-anchor-bar { .emoji-mart-anchor-bar {
@ -95,7 +94,7 @@
height: 270px; height: 270px;
max-height: 35vh; max-height: 35vh;
padding: 0 6px 6px; padding: 0 6px 6px;
background: $simple-background-color; background: var(--dropdown-background-color);
will-change: transform; will-change: transform;
&::-webkit-scrollbar-track:hover, &::-webkit-scrollbar-track:hover,
@ -107,7 +106,7 @@
.emoji-mart-search { .emoji-mart-search {
padding: 10px; padding: 10px;
padding-inline-end: 45px; padding-inline-end: 45px;
background: $simple-background-color; background: var(--dropdown-background-color);
position: relative; position: relative;
input { input {
@ -118,9 +117,9 @@
font-family: inherit; font-family: inherit;
display: block; display: block;
width: 100%; width: 100%;
background: rgba($ui-secondary-color, 0.3); background: $ui-base-color;
color: $inverted-text-color; color: $darker-text-color;
border: 1px solid $ui-secondary-color; border: 1px solid lighten($ui-base-color, 8%);
border-radius: 4px; border-radius: 4px;
&::-moz-focus-inner { &::-moz-focus-inner {
@ -155,11 +154,10 @@
&:disabled { &:disabled {
cursor: default; cursor: default;
pointer-events: none; pointer-events: none;
opacity: 0.3;
} }
svg { svg {
fill: $action-button-color; fill: $darker-text-color;
} }
} }
@ -185,7 +183,7 @@
inset-inline-start: 0; inset-inline-start: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-color: rgba($ui-secondary-color, 0.7); background-color: var(--dropdown-border-color);
border-radius: 100%; border-radius: 100%;
} }
} }
@ -202,7 +200,7 @@
width: 100%; width: 100%;
font-weight: 500; font-weight: 500;
padding: 5px 6px; padding: 5px 6px;
background: $simple-background-color; background: var(--dropdown-background-color);
} }
} }
@ -246,7 +244,7 @@
.emoji-mart-no-results { .emoji-mart-no-results {
font-size: 14px; font-size: 14px;
color: $light-text-color; color: $dark-text-color;
text-align: center; text-align: center;
padding: 5px 6px; padding: 5px 6px;
padding-top: 70px; padding-top: 70px;

View file

@ -1,5 +1,5 @@
.modal-layout { .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>') 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; repeat-x bottom fixed;
display: flex; display: flex;

View file

@ -52,6 +52,8 @@
&__option { &__option {
position: relative; position: relative;
display: flex; display: flex;
align-items: flex-start;
gap: 8px;
padding: 6px 0; padding: 6px 0;
line-height: 18px; line-height: 18px;
cursor: default; cursor: default;
@ -78,16 +80,22 @@
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
font-size: 14px; font-size: 14px;
color: $inverted-text-color; color: $secondary-text-color;
outline: 0; outline: 0;
font-family: inherit; font-family: inherit;
background: $simple-background-color; background: $ui-base-color;
border: 1px solid darken($simple-background-color, 14%); border: 1px solid $darker-text-color;
border-radius: 4px; border-radius: 4px;
padding: 6px 10px; padding: 8px 12px;
&:focus { &: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 { &.editable {
display: flex;
align-items: center; align-items: center;
overflow: visible; overflow: visible;
} }
} }
&__input { &__input {
display: inline-block; display: block;
position: relative; position: relative;
border: 1px solid $ui-primary-color; border: 1px solid $ui-primary-color;
box-sizing: border-box; box-sizing: border-box;
width: 18px; width: 17px;
height: 18px; height: 17px;
margin-inline-end: 10px;
top: -1px;
border-radius: 50%; border-radius: 50%;
vertical-align: middle; flex: 0 0 auto;
margin-top: auto;
margin-bottom: auto;
flex: 0 0 18px;
&.checkbox { &.checkbox {
border-radius: 4px; border-radius: 4px;
@ -159,6 +161,15 @@
} }
} }
&__option.editable &__input {
&:active,
&:focus,
&:hover {
border-color: $ui-primary-color;
border-width: 1px;
}
}
&__number { &__number {
display: inline-block; display: inline-block;
width: 45px; 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 { .muted .poll {
color: $dark-text-color; color: $dark-text-color;

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