Merge remote-tracking branch 'parent/main' into upstream-20250328
This commit is contained in:
commit
12ed20b6d5
257 changed files with 3505 additions and 2010 deletions
|
@ -21,12 +21,13 @@ services:
|
|||
ES_HOST: es
|
||||
ES_PORT: '9200'
|
||||
LIBRE_TRANSLATE_ENDPOINT: http://libretranslate:5000
|
||||
LOCAL_DOMAIN: ${LOCAL_DOMAIN:-localhost:3000}
|
||||
# Overrides default command so things don't shut down after the process ends.
|
||||
command: sleep infinity
|
||||
ports:
|
||||
- '127.0.0.1:3000:3000'
|
||||
- '127.0.0.1:3035:3035'
|
||||
- '127.0.0.1:4000:4000'
|
||||
- '3000:3000'
|
||||
- '3035:3035'
|
||||
- '4000:4000'
|
||||
networks:
|
||||
- external_network
|
||||
- internal_network
|
||||
|
|
|
@ -79,6 +79,9 @@ AWS_ACCESS_KEY_ID=
|
|||
AWS_SECRET_ACCESS_KEY=
|
||||
S3_ALIAS_HOST=files.example.com
|
||||
|
||||
# Optional list of hosts that are allowed to serve media for your instance
|
||||
# EXTRA_MEDIA_HOSTS=https://data.example1.com,https://data.example2.com
|
||||
|
||||
# IP and session retention
|
||||
# -----------------------
|
||||
# Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
|
||||
# using RuboCop version 1.73.2.
|
||||
# using RuboCop version 1.75.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
|
@ -49,7 +49,7 @@ Style/FetchEnvVar:
|
|||
- 'lib/tasks/repo.rake'
|
||||
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, AllowedMethods, AllowedPatterns.
|
||||
# Configuration parameters: EnforcedStyle, MaxUnannotatedPlaceholdersAllowed, Mode, AllowedMethods, AllowedPatterns.
|
||||
# SupportedStyles: annotated, template, unannotated
|
||||
# AllowedMethods: redirect
|
||||
Style/FormatStringToken:
|
||||
|
@ -77,7 +77,6 @@ Style/MapToHash:
|
|||
# AllowedMethods: respond_to_missing?
|
||||
Style/OptionalBooleanParameter:
|
||||
Exclude:
|
||||
- 'app/helpers/json_ld_helper.rb'
|
||||
- 'app/lib/admin/system_check/message.rb'
|
||||
- 'app/lib/request.rb'
|
||||
- 'app/lib/webfinger.rb'
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -14,6 +14,7 @@ gem 'haml-rails', '~>2.0'
|
|||
gem 'pg', '~> 1.5'
|
||||
gem 'pghero'
|
||||
|
||||
gem 'aws-sdk-core', '< 3.216.0', require: false # TODO: https://github.com/mastodon/mastodon/pull/34173#issuecomment-2733378873
|
||||
gem 'aws-sdk-s3', '~> 1.123', require: false
|
||||
gem 'blurhash', '~> 0.1'
|
||||
gem 'fog-core', '<= 2.6.0'
|
||||
|
|
65
Gemfile.lock
65
Gemfile.lock
|
@ -91,11 +91,11 @@ GEM
|
|||
aes_key_wrap (1.1.0)
|
||||
android_key_attestation (0.3.0)
|
||||
annotaterb (4.14.0)
|
||||
ast (2.4.2)
|
||||
ast (2.4.3)
|
||||
attr_required (1.0.2)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1032.0)
|
||||
aws-sdk-core (3.214.1)
|
||||
aws-eventstream (1.3.2)
|
||||
aws-partitions (1.1066.0)
|
||||
aws-sdk-core (3.215.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
|
@ -107,9 +107,9 @@ GEM
|
|||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-sigv4 (1.11.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
azure-blob (0.5.4)
|
||||
azure-blob (0.5.7)
|
||||
rexml
|
||||
base64 (0.2.0)
|
||||
bcp47_spec (0.2.1)
|
||||
|
@ -168,9 +168,9 @@ GEM
|
|||
bigdecimal
|
||||
rexml
|
||||
crass (1.0.6)
|
||||
css_parser (1.21.0)
|
||||
css_parser (1.21.1)
|
||||
addressable
|
||||
csv (3.3.2)
|
||||
csv (3.3.3)
|
||||
database_cleaner-active_record (2.2.0)
|
||||
activerecord (>= 5.a)
|
||||
database_cleaner-core (~> 2.0.0)
|
||||
|
@ -222,7 +222,8 @@ GEM
|
|||
erubi (1.13.1)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
excon (1.2.3)
|
||||
excon (1.2.5)
|
||||
logger
|
||||
fabrication (2.31.0)
|
||||
faker (3.5.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
|
@ -256,7 +257,7 @@ GEM
|
|||
fog-json (1.2.0)
|
||||
fog-core
|
||||
multi_json (~> 1.10)
|
||||
fog-openstack (1.1.4)
|
||||
fog-openstack (1.1.5)
|
||||
fog-core (~> 2.1)
|
||||
fog-json (>= 1.0)
|
||||
formatador (1.1.0)
|
||||
|
@ -265,7 +266,9 @@ GEM
|
|||
raabro (~> 1.4)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
google-protobuf (3.25.6)
|
||||
google-protobuf (4.30.1)
|
||||
bigdecimal
|
||||
rake (>= 13)
|
||||
googleapis-common-protos-types (1.18.0)
|
||||
google-protobuf (>= 3.18, < 5.a)
|
||||
haml (6.3.0)
|
||||
|
@ -277,7 +280,7 @@ GEM
|
|||
activesupport (>= 5.1)
|
||||
haml (>= 4.0.6)
|
||||
railties (>= 5.1)
|
||||
haml_lint (0.61.0)
|
||||
haml_lint (0.61.1)
|
||||
haml (>= 5.0)
|
||||
parallel (~> 1.10)
|
||||
rainbow
|
||||
|
@ -302,7 +305,8 @@ GEM
|
|||
domain_name (~> 0.5)
|
||||
http-form_data (2.3.0)
|
||||
http_accept_language (2.1.1)
|
||||
httpclient (2.8.3)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
httplog (1.7.0)
|
||||
rack (>= 2.0)
|
||||
rainbow (>= 2.0.0)
|
||||
|
@ -378,7 +382,7 @@ GEM
|
|||
mime-types
|
||||
terrapin (>= 0.6.0, < 2.0)
|
||||
language_server-protocol (3.17.0.4)
|
||||
launchy (3.1.0)
|
||||
launchy (3.1.1)
|
||||
addressable (~> 2.8)
|
||||
childprocess (~> 5.0)
|
||||
logger (~> 1.6)
|
||||
|
@ -391,7 +395,7 @@ GEM
|
|||
rexml
|
||||
link_header (0.0.8)
|
||||
lint_roller (1.1.0)
|
||||
llhttp-ffi (0.5.0)
|
||||
llhttp-ffi (0.5.1)
|
||||
ffi-compiler (~> 1.0)
|
||||
rake (~> 13.0)
|
||||
logger (1.6.6)
|
||||
|
@ -413,13 +417,13 @@ GEM
|
|||
redis (>= 3.0.5)
|
||||
matrix (0.4.2)
|
||||
memory_profiler (1.1.0)
|
||||
mime-types (3.6.0)
|
||||
mime-types (3.6.2)
|
||||
logger
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2025.0220)
|
||||
mime-types-data (3.2025.0318)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.8)
|
||||
minitest (5.25.4)
|
||||
minitest (5.25.5)
|
||||
msgpack (1.8.0)
|
||||
multi_json (1.15.0)
|
||||
mutex_m (0.3.0)
|
||||
|
@ -436,7 +440,7 @@ GEM
|
|||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.4)
|
||||
nokogiri (1.18.3)
|
||||
nokogiri (1.18.6)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.10)
|
||||
|
@ -576,14 +580,14 @@ GEM
|
|||
ox (2.14.22)
|
||||
bigdecimal (>= 3.0)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.7.1)
|
||||
parser (3.3.7.3)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
parslet (2.0.0)
|
||||
pastel (0.8.0)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.5.9)
|
||||
pghero (3.6.1)
|
||||
pghero (3.6.2)
|
||||
activerecord (>= 6.1)
|
||||
pp (0.6.2)
|
||||
prettyprint
|
||||
|
@ -596,6 +600,7 @@ GEM
|
|||
net-smtp
|
||||
premailer (~> 1.7, >= 1.7.9)
|
||||
prettyprint (0.2.0)
|
||||
prism (1.4.0)
|
||||
prometheus_exporter (2.2.0)
|
||||
webrick
|
||||
propshaft (1.1.0)
|
||||
|
@ -729,7 +734,7 @@ GEM
|
|||
rspec-mocks (~> 3.0)
|
||||
sidekiq (>= 5, < 9)
|
||||
rspec-support (3.13.2)
|
||||
rubocop (1.74.0)
|
||||
rubocop (1.75.1)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
|
@ -737,11 +742,12 @@ GEM
|
|||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.38.0, < 2.0)
|
||||
rubocop-ast (>= 1.43.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.38.1)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-ast (1.43.0)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.4)
|
||||
rubocop-capybara (2.22.1)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
|
@ -785,7 +791,7 @@ GEM
|
|||
activerecord (>= 4.0.0)
|
||||
railties (>= 4.0.0)
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.29.1)
|
||||
selenium-webdriver (4.30.1)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
|
@ -826,7 +832,7 @@ GEM
|
|||
stoplight (4.1.1)
|
||||
redlock (~> 1.0)
|
||||
stringio (3.1.5)
|
||||
strong_migrations (2.2.0)
|
||||
strong_migrations (2.2.1)
|
||||
activerecord (>= 7)
|
||||
swd (2.0.3)
|
||||
activesupport (>= 3)
|
||||
|
@ -862,7 +868,7 @@ GEM
|
|||
unf (~> 0.1.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
tzinfo-data (1.2025.1)
|
||||
tzinfo-data (1.2025.2)
|
||||
tzinfo (>= 1.0.0)
|
||||
unf (0.1.4)
|
||||
unf_ext
|
||||
|
@ -917,6 +923,7 @@ DEPENDENCIES
|
|||
active_model_serializers (~> 0.10)
|
||||
addressable (~> 2.8)
|
||||
annotaterb (~> 4.13)
|
||||
aws-sdk-core (< 3.216.0)
|
||||
aws-sdk-s3 (~> 1.123)
|
||||
better_errors (~> 2.9)
|
||||
binding_of_caller (~> 1.0)
|
||||
|
@ -1069,4 +1076,4 @@ RUBY VERSION
|
|||
ruby 3.4.1p0
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.5
|
||||
2.6.6
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Accounts::IdentityProofsController < Api::BaseController
|
||||
include DeprecationConcern
|
||||
|
||||
deprecate_api '2022-03-30'
|
||||
|
||||
before_action :require_user!
|
||||
before_action :set_account
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||
end
|
||||
|
||||
def account_params
|
||||
params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code)
|
||||
params.permit(:username, :email, :password, :agreement, :locale, :reason, :time_zone, :invite_code, :date_of_birth)
|
||||
end
|
||||
|
||||
def invite
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::FiltersController < Api::BaseController
|
||||
include DeprecationConcern
|
||||
|
||||
deprecate_api '2022-11-14'
|
||||
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:filters' }, only: [:index, :show]
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:filters' }, except: [:index, :show]
|
||||
before_action :require_user!
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::InstancesController < Api::BaseController
|
||||
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
|
||||
skip_around_action :set_locale
|
||||
class Api::V1::InstancesController < Api::V2::InstancesController
|
||||
include DeprecationConcern
|
||||
|
||||
vary_by ''
|
||||
|
||||
# Override `current_user` to avoid reading session cookies unless in limited federation mode
|
||||
def current_user
|
||||
super if limited_federation_mode?
|
||||
end
|
||||
deprecate_api '2022-11-14'
|
||||
|
||||
def show
|
||||
cache_even_if_authenticated!
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
class Api::V1::SuggestionsController < Api::BaseController
|
||||
include Authorization
|
||||
include DeprecationConcern
|
||||
|
||||
deprecate_api '2021-05-16'
|
||||
|
||||
before_action -> { doorkeeper_authorize! :read, :'read:accounts' }, only: :index
|
||||
before_action -> { doorkeeper_authorize! :write, :'write:accounts' }, except: :index
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Trends::TagsController < Api::BaseController
|
||||
include DeprecationConcern
|
||||
|
||||
before_action :set_tags
|
||||
|
||||
after_action :insert_pagination_headers
|
||||
|
||||
DEFAULT_TAGS_LIMIT = 10
|
||||
|
||||
deprecate_api '2022-03-30', only: :index, if: -> { request.path == '/api/v1/trends' }
|
||||
|
||||
def index
|
||||
cache_if_unauthenticated!
|
||||
render json: @tags, each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@tags, current_user&.account_id)
|
||||
|
|
|
@ -1,6 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V2::InstancesController < Api::V1::InstancesController
|
||||
class Api::V2::InstancesController < Api::BaseController
|
||||
skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
|
||||
skip_around_action :set_locale
|
||||
|
||||
vary_by ''
|
||||
|
||||
# Override `current_user` to avoid reading session cookies unless in limited federation mode
|
||||
def current_user
|
||||
super if limited_federation_mode?
|
||||
end
|
||||
|
||||
def show
|
||||
cache_even_if_authenticated!
|
||||
render_with_cache json: InstancePresenter.new, serializer: REST::InstanceSerializer, root: 'instance'
|
||||
|
|
|
@ -62,7 +62,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
|
||||
def configure_sign_up_params
|
||||
devise_parameter_sanitizer.permit(:sign_up) do |user_params|
|
||||
user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password)
|
||||
user_params.permit({ account_attributes: [:username, :display_name], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password, :date_of_birth)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -9,13 +9,15 @@ class BackupsController < ApplicationController
|
|||
before_action :authenticate_user!
|
||||
before_action :set_backup
|
||||
|
||||
BACKUP_LINK_TIMEOUT = 1.hour.freeze
|
||||
|
||||
def download
|
||||
case Paperclip::Attachment.default_options[:storage]
|
||||
when :s3, :azure
|
||||
redirect_to @backup.dump.expiring_url(10), allow_other_host: true
|
||||
redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.to_i), allow_other_host: true
|
||||
when :fog
|
||||
if Paperclip::Attachment.default_options.dig(:fog_credentials, :openstack_temp_url_key).present?
|
||||
redirect_to @backup.dump.expiring_url(Time.now.utc + 10), allow_other_host: true
|
||||
redirect_to @backup.dump.expiring_url(BACKUP_LINK_TIMEOUT.from_now), allow_other_host: true
|
||||
else
|
||||
redirect_to full_asset_url(@backup.dump.url), allow_other_host: true
|
||||
end
|
||||
|
|
17
app/controllers/concerns/deprecation_concern.rb
Normal file
17
app/controllers/concerns/deprecation_concern.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module DeprecationConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def deprecate_api(date, sunset: nil, **kwargs)
|
||||
deprecation_timestamp = "@#{date.to_datetime.to_i}"
|
||||
sunset = sunset&.to_date&.httpdate
|
||||
|
||||
before_action(**kwargs) do
|
||||
response.headers['Deprecation'] = deprecation_timestamp
|
||||
response.headers['Sunset'] = sunset if sunset
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2,11 +2,18 @@
|
|||
|
||||
module Admin::Trends::StatusesHelper
|
||||
def one_line_preview(status)
|
||||
text = if status.local?
|
||||
status.text.split("\n").first
|
||||
else
|
||||
Nokogiri::HTML5(status.text).css('html > body > *').first&.text
|
||||
end
|
||||
text = begin
|
||||
if status.local?
|
||||
status.text.split("\n").first
|
||||
else
|
||||
Nokogiri::HTML5(status.text).css('html > body > *').first&.text
|
||||
end
|
||||
rescue ArgumentError
|
||||
# This can happen if one of the Nokogumbo limits is encountered
|
||||
# Unfortunately, it does not use a more precise error class
|
||||
# nor allows more graceful handling
|
||||
''
|
||||
end
|
||||
|
||||
return '' if text.blank?
|
||||
|
||||
|
|
31
app/inputs/date_of_birth_input.rb
Normal file
31
app/inputs/date_of_birth_input.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DateOfBirthInput < SimpleForm::Inputs::Base
|
||||
OPTIONS = [
|
||||
{ autocomplete: 'bday-day', maxlength: 2, pattern: '[0-9]+', placeholder: 'DD' }.freeze,
|
||||
{ autocomplete: 'bday-month', maxlength: 2, pattern: '[0-9]+', placeholder: 'MM' }.freeze,
|
||||
{ autocomplete: 'bday-year', maxlength: 4, pattern: '[0-9]+', placeholder: 'YYYY' }.freeze,
|
||||
].freeze
|
||||
|
||||
def input(wrapper_options = nil)
|
||||
merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
|
||||
merged_input_options[:inputmode] = 'numeric'
|
||||
|
||||
values = (object.public_send(attribute_name) || '').split('.')
|
||||
|
||||
safe_join(Array.new(3) do |index|
|
||||
options = merged_input_options.merge(OPTIONS[index]).merge id: generate_id(index), 'aria-label': I18n.t("simple_form.labels.user.date_of_birth_#{index + 1}i"), value: values[index]
|
||||
@builder.text_field("#{attribute_name}(#{index + 1}i)", options)
|
||||
end)
|
||||
end
|
||||
|
||||
def label_target
|
||||
"#{attribute_name}_1i"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_id(index)
|
||||
"#{object_name}_#{attribute_name}_#{index + 1}i"
|
||||
end
|
||||
end
|
|
@ -1,14 +1,11 @@
|
|||
import { defineMessages } from 'react-intl';
|
||||
import type { MessageDescriptor } from 'react-intl';
|
||||
|
||||
import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { AxiosError } from 'axios';
|
||||
import type { AxiosResponse } from 'axios';
|
||||
|
||||
interface Alert {
|
||||
title: string | MessageDescriptor;
|
||||
message: string | MessageDescriptor;
|
||||
values?: Record<string, string | number | Date>;
|
||||
}
|
||||
import type { Alert } from 'mastodon/models/alert';
|
||||
|
||||
interface ApiErrorResponse {
|
||||
error?: string;
|
||||
|
@ -30,24 +27,13 @@ const messages = defineMessages({
|
|||
},
|
||||
});
|
||||
|
||||
export const ALERT_SHOW = 'ALERT_SHOW';
|
||||
export const ALERT_DISMISS = 'ALERT_DISMISS';
|
||||
export const ALERT_CLEAR = 'ALERT_CLEAR';
|
||||
export const ALERT_NOOP = 'ALERT_NOOP';
|
||||
export const dismissAlert = createAction<{ key: number }>('alerts/dismiss');
|
||||
|
||||
export const dismissAlert = (alert: Alert) => ({
|
||||
type: ALERT_DISMISS,
|
||||
alert,
|
||||
});
|
||||
export const clearAlerts = createAction('alerts/clear');
|
||||
|
||||
export const clearAlert = () => ({
|
||||
type: ALERT_CLEAR,
|
||||
});
|
||||
export const showAlert = createAction<Omit<Alert, 'key'>>('alerts/show');
|
||||
|
||||
export const showAlert = (alert: Alert) => ({
|
||||
type: ALERT_SHOW,
|
||||
alert,
|
||||
});
|
||||
const ignoreAlert = createAction('alerts/ignore');
|
||||
|
||||
export const showAlertForError = (error: unknown, skipNotFound = false) => {
|
||||
if (error instanceof AxiosError && error.response) {
|
||||
|
@ -56,7 +42,7 @@ export const showAlertForError = (error: unknown, skipNotFound = false) => {
|
|||
|
||||
// Skip these errors as they are reflected in the UI
|
||||
if (skipNotFound && (status === 404 || status === 410)) {
|
||||
return { type: ALERT_NOOP };
|
||||
return ignoreAlert();
|
||||
}
|
||||
|
||||
// Rate limit errors
|
||||
|
@ -76,9 +62,9 @@ export const showAlertForError = (error: unknown, skipNotFound = false) => {
|
|||
});
|
||||
}
|
||||
|
||||
// An aborted request, e.g. due to reloading the browser window, it not really error
|
||||
// An aborted request, e.g. due to reloading the browser window, is not really an error
|
||||
if (error instanceof AxiosError && error.code === AxiosError.ECONNABORTED) {
|
||||
return { type: ALERT_NOOP };
|
||||
return ignoreAlert();
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
import type { AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios';
|
||||
import type {
|
||||
AxiosError,
|
||||
AxiosResponse,
|
||||
Method,
|
||||
RawAxiosRequestHeaders,
|
||||
} from 'axios';
|
||||
import axios from 'axios';
|
||||
import LinkHeader from 'http-link-header';
|
||||
|
||||
|
@ -41,7 +46,7 @@ const authorizationTokenFromInitialState = (): RawAxiosRequestHeaders => {
|
|||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function api(withAuthorization = true) {
|
||||
return axios.create({
|
||||
const instance = axios.create({
|
||||
transitional: {
|
||||
clarifyTimeoutError: true,
|
||||
},
|
||||
|
@ -60,6 +65,22 @@ export default function api(withAuthorization = true) {
|
|||
},
|
||||
],
|
||||
});
|
||||
|
||||
instance.interceptors.response.use(
|
||||
(response: AxiosResponse) => {
|
||||
if (response.headers.deprecation) {
|
||||
console.warn(
|
||||
`Deprecated request: ${response.config.method} ${response.config.url}`,
|
||||
);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
type RequestParamsOrData = Record<string, unknown>;
|
||||
|
|
105
app/javascript/mastodon/components/alerts_controller.tsx
Normal file
105
app/javascript/mastodon/components/alerts_controller.tsx
Normal file
|
@ -0,0 +1,105 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
import type { IntlShape } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { dismissAlert } from 'mastodon/actions/alerts';
|
||||
import type {
|
||||
Alert,
|
||||
TranslatableString,
|
||||
TranslatableValues,
|
||||
} from 'mastodon/models/alert';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
const formatIfNeeded = (
|
||||
intl: IntlShape,
|
||||
message: TranslatableString,
|
||||
values?: TranslatableValues,
|
||||
) => {
|
||||
if (typeof message === 'object') {
|
||||
return intl.formatMessage(message, values);
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
const Alert: React.FC<{
|
||||
alert: Alert;
|
||||
dismissAfter: number;
|
||||
}> = ({
|
||||
alert: { key, title, message, values, action, onClick },
|
||||
dismissAfter,
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const [active, setActive] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const setActiveTimeout = setTimeout(() => {
|
||||
setActive(true);
|
||||
}, 1);
|
||||
|
||||
return () => {
|
||||
clearTimeout(setActiveTimeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const dismissTimeout = setTimeout(() => {
|
||||
setActive(false);
|
||||
|
||||
// Allow CSS transition to finish before removing from the DOM
|
||||
setTimeout(() => {
|
||||
dispatch(dismissAlert({ key }));
|
||||
}, 500);
|
||||
}, dismissAfter);
|
||||
|
||||
return () => {
|
||||
clearTimeout(dismissTimeout);
|
||||
};
|
||||
}, [dispatch, setActive, key, dismissAfter]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('notification-bar', {
|
||||
'notification-bar-active': active,
|
||||
})}
|
||||
>
|
||||
<div className='notification-bar-wrapper'>
|
||||
{title && (
|
||||
<span className='notification-bar-title'>
|
||||
{formatIfNeeded(intl, title, values)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className='notification-bar-message'>
|
||||
{formatIfNeeded(intl, message, values)}
|
||||
</span>
|
||||
|
||||
{action && (
|
||||
<button className='notification-bar-action' onClick={onClick}>
|
||||
{formatIfNeeded(intl, action, values)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AlertsController: React.FC = () => {
|
||||
const alerts = useAppSelector((state) => state.alerts);
|
||||
|
||||
if (alerts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='notification-list'>
|
||||
{alerts.map((alert, idx) => (
|
||||
<Alert key={alert.key} alert={alert} dismissAfter={5000 + idx * 1000} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -6,6 +6,7 @@ import { FormattedMessage, injectIntl } from 'react-intl';
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
|
||||
import InlineAccount from 'mastodon/components/inline_account';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
|
||||
|
@ -60,12 +61,12 @@ class EditedTimestamp extends PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { timestamp, intl, statusId } = this.props;
|
||||
const { timestamp, statusId } = this.props;
|
||||
|
||||
return (
|
||||
<DropdownMenu statusId={statusId} renderItem={this.renderItem} scrollable renderHeader={this.renderHeader} onItemClick={this.handleItemClick}>
|
||||
<button className='dropdown-menu__text-button'>
|
||||
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: <span className='animated-number'>{intl.formatDate(timestamp, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</span> }} />
|
||||
<FormattedMessage id='status.edited' defaultMessage='Edited {date}' values={{ date: <FormattedDateWrapper className='animated-number' value={timestamp} month='short' day='2-digit' hour='2-digit' minute='2-digit' /> }} />
|
||||
</button>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
|
26
app/javascript/mastodon/components/formatted_date.tsx
Normal file
26
app/javascript/mastodon/components/formatted_date.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import type { ComponentProps } from 'react';
|
||||
|
||||
import { FormattedDate } from 'react-intl';
|
||||
|
||||
export const FormattedDateWrapper = (
|
||||
props: ComponentProps<typeof FormattedDate> & { className?: string },
|
||||
) => (
|
||||
<FormattedDate {...props}>
|
||||
{(date) => (
|
||||
<time dateTime={tryIsoString(props.value)} className={props.className}>
|
||||
{date}
|
||||
</time>
|
||||
)}
|
||||
</FormattedDate>
|
||||
);
|
||||
|
||||
const tryIsoString = (date?: string | number | Date): string => {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return new Date(date).toISOString();
|
||||
} catch {
|
||||
return date.toString();
|
||||
}
|
||||
};
|
|
@ -12,6 +12,7 @@ import { debounce } from 'lodash';
|
|||
|
||||
import { AltTextBadge } from 'mastodon/components/alt_text_badge';
|
||||
import { Blurhash } from 'mastodon/components/blurhash';
|
||||
import { SpoilerButton } from 'mastodon/components/spoiler_button';
|
||||
import { formatTime } from 'mastodon/features/video';
|
||||
|
||||
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
|
||||
|
@ -38,6 +39,7 @@ class Item extends PureComponent {
|
|||
|
||||
state = {
|
||||
loaded: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
handleMouseEnter = (e) => {
|
||||
|
@ -81,6 +83,10 @@ class Item extends PureComponent {
|
|||
this.setState({ loaded: true });
|
||||
};
|
||||
|
||||
handleImageError = () => {
|
||||
this.setState({ error: true });
|
||||
};
|
||||
|
||||
render () {
|
||||
const { attachment, lang, index, size, standalone, displayWidth, visible } = this.props;
|
||||
|
||||
|
@ -164,6 +170,7 @@ class Item extends PureComponent {
|
|||
lang={lang}
|
||||
style={{ objectPosition: `${x}% ${y}%` }}
|
||||
onLoad={this.handleImageLoad}
|
||||
onError={this.handleImageError}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
|
@ -199,7 +206,7 @@ class Item extends PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
||||
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--error': this.state.error, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
||||
<Blurhash
|
||||
hash={attachment.get('blurhash')}
|
||||
dummy={!useBlurhash}
|
||||
|
@ -236,6 +243,7 @@ class MediaGallery extends PureComponent {
|
|||
autoplay: PropTypes.bool,
|
||||
onToggleVisibility: PropTypes.func,
|
||||
compact: PropTypes.bool,
|
||||
matchedFilters: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -306,11 +314,11 @@ class MediaGallery extends PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { media, lang, sensitive, defaultWidth, autoplay, compact } = this.props;
|
||||
const { media, lang, sensitive, defaultWidth, autoplay, compact, matchedFilters } = this.props;
|
||||
const { visible } = this.state;
|
||||
const width = this.state.width || defaultWidth;
|
||||
|
||||
let children, spoilerButton;
|
||||
let children;
|
||||
|
||||
const style = {};
|
||||
|
||||
|
@ -329,26 +337,6 @@ class MediaGallery extends PureComponent {
|
|||
children = media.map((attachment, i) => <Item key={attachment.get('id')} autoplay={autoplay} onClick={this.handleClick} attachment={attachment} index={i} lang={lang} size={size} displayWidth={width} visible={visible || uncached} />);
|
||||
}
|
||||
|
||||
if (uncached) {
|
||||
spoilerButton = (
|
||||
<button type='button' disabled className='spoiler-button__overlay'>
|
||||
<span className='spoiler-button__overlay__label'>
|
||||
<FormattedMessage id='status.uncached_media_warning' defaultMessage='Preview not available' />
|
||||
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.open' defaultMessage='Click to open' /></span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
} else if (!visible) {
|
||||
spoilerButton = (
|
||||
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
|
||||
<span className='spoiler-button__overlay__label'>
|
||||
{sensitive ? <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' /> : <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />}
|
||||
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const rowClass = (size === 5 || size === 6 || size === 9 || size === 10 || size === 11 || size === 12) ? 'media-gallery--row3' :
|
||||
(size === 7 || size === 8 || size === 13 || size === 14 || size === 15 || size === 16) ? 'media-gallery--row4' :
|
||||
'media-gallery--row2';
|
||||
|
@ -366,11 +354,7 @@ class MediaGallery extends PureComponent {
|
|||
<div className={classNames(classList)} style={style} ref={this.handleRef}>
|
||||
{children}
|
||||
|
||||
{(!visible || uncached) && (
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}>
|
||||
{spoilerButton}
|
||||
</div>
|
||||
)}
|
||||
{(!visible || uncached) && <SpoilerButton uncached={uncached} sensitive={sensitive} onClick={this.handleOpen} matchedFilters={matchedFilters} />}
|
||||
|
||||
{(visible && !uncached) && (
|
||||
<div className='media-gallery__actions'>
|
||||
|
|
89
app/javascript/mastodon/components/spoiler_button.tsx
Normal file
89
app/javascript/mastodon/components/spoiler_button.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props {
|
||||
hidden?: boolean;
|
||||
sensitive: boolean;
|
||||
uncached?: boolean;
|
||||
matchedFilters?: string[];
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
export const SpoilerButton: React.FC<Props> = ({
|
||||
hidden = false,
|
||||
sensitive,
|
||||
uncached = false,
|
||||
matchedFilters,
|
||||
onClick,
|
||||
}) => {
|
||||
let warning;
|
||||
let action;
|
||||
|
||||
if (uncached) {
|
||||
warning = (
|
||||
<FormattedMessage
|
||||
id='status.uncached_media_warning'
|
||||
defaultMessage='Preview not available'
|
||||
/>
|
||||
);
|
||||
action = (
|
||||
<FormattedMessage id='status.media.open' defaultMessage='Click to open' />
|
||||
);
|
||||
} else if (matchedFilters) {
|
||||
warning = (
|
||||
<FormattedMessage
|
||||
id='filter_warning.matches_filter'
|
||||
defaultMessage='Matches filter “<span>{title}</span>”'
|
||||
values={{
|
||||
title: matchedFilters.join(', '),
|
||||
span: (chunks) => <span className='filter-name'>{chunks}</span>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
action = (
|
||||
<FormattedMessage id='status.media.show' defaultMessage='Click to show' />
|
||||
);
|
||||
} else if (sensitive) {
|
||||
warning = (
|
||||
<FormattedMessage
|
||||
id='status.sensitive_warning'
|
||||
defaultMessage='Sensitive content'
|
||||
/>
|
||||
);
|
||||
action = (
|
||||
<FormattedMessage id='status.media.show' defaultMessage='Click to show' />
|
||||
);
|
||||
} else {
|
||||
warning = (
|
||||
<FormattedMessage
|
||||
id='status.media_hidden'
|
||||
defaultMessage='Media hidden'
|
||||
/>
|
||||
);
|
||||
action = (
|
||||
<FormattedMessage id='status.media.show' defaultMessage='Click to show' />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('spoiler-button', {
|
||||
'spoiler-button--hidden': hidden,
|
||||
'spoiler-button--click-thru': uncached,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='spoiler-button__overlay'
|
||||
onClick={onClick}
|
||||
disabled={uncached}
|
||||
>
|
||||
<span className='spoiler-button__overlay__label'>
|
||||
{warning}
|
||||
<span className='spoiler-button__overlay__action'>{action}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -77,7 +77,7 @@ export const defaultMediaVisibility = (status) => {
|
|||
status = status.get('reblog');
|
||||
}
|
||||
|
||||
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
|
||||
return !status.get('matched_media_filters') && (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -496,6 +496,7 @@ class Status extends ImmutablePureComponent {
|
|||
defaultWidth={this.props.cachedMediaWidth}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
matchedFilters={status.get('matched_media_filters')}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
|
@ -524,6 +525,7 @@ class Status extends ImmutablePureComponent {
|
|||
blurhash={attachment.get('blurhash')}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
matchedFilters={status.get('matched_media_filters')}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
|
@ -548,6 +550,7 @@ class Status extends ImmutablePureComponent {
|
|||
deployPictureInPicture={pictureInPicture.get('available') ? this.handleDeployPictureInPicture : undefined}
|
||||
visible={this.state.showMedia}
|
||||
onToggleVisibility={this.handleToggleMediaVisibility}
|
||||
matchedFilters={status.get('matched_media_filters')}
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
|
|
|
@ -4,7 +4,6 @@ import { PureComponent } from 'react';
|
|||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { is } from 'immutable';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import Textarea from 'react-textarea-autosize';
|
||||
|
@ -49,7 +48,7 @@ class InlineAlert extends PureComponent {
|
|||
class AccountNote extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
accountId: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
onSave: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
|
@ -66,7 +65,7 @@ class AccountNote extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
const accountWillChange = !is(this.props.account, nextProps.account);
|
||||
const accountWillChange = !is(this.props.accountId, nextProps.accountId);
|
||||
const newState = {};
|
||||
|
||||
if (accountWillChange && this._isDirty()) {
|
||||
|
@ -102,10 +101,10 @@ class AccountNote extends ImmutablePureComponent {
|
|||
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
|
||||
this._save();
|
||||
|
||||
if (this.textarea) {
|
||||
this.textarea.blur();
|
||||
} else {
|
||||
this._save();
|
||||
}
|
||||
} else if (e.keyCode === 27) {
|
||||
e.preventDefault();
|
||||
|
@ -141,21 +140,21 @@ class AccountNote extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { account, intl } = this.props;
|
||||
const { accountId, intl } = this.props;
|
||||
const { value, saved } = this.state;
|
||||
|
||||
if (!account) {
|
||||
if (!accountId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account__header__account-note'>
|
||||
<label htmlFor={`account-note-${account.get('id')}`}>
|
||||
<label htmlFor={`account-note-${accountId}`}>
|
||||
<FormattedMessage id='account.account_note_header' defaultMessage='Personal note' /> <InlineAlert show={saved} />
|
||||
</label>
|
||||
|
||||
<Textarea
|
||||
id={`account-note-${account.get('id')}`}
|
||||
id={`account-note-${accountId}`}
|
||||
className='account__header__account-note__content'
|
||||
disabled={this.props.value === null || value === null}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
|
|
|
@ -4,14 +4,14 @@ import { submitAccountNote } from 'mastodon/actions/account_notes';
|
|||
|
||||
import AccountNote from '../components/account_note';
|
||||
|
||||
const mapStateToProps = (state, { account }) => ({
|
||||
value: account.getIn(['relationship', 'note']),
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
value: state.relationships.getIn([accountId, 'note']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { account }) => ({
|
||||
const mapDispatchToProps = (dispatch, { accountId }) => ({
|
||||
|
||||
onSave (value) {
|
||||
dispatch(submitAccountNote({ accountId: account.get('id'), note: value }));
|
||||
dispatch(submitAccountNote({ accountId: accountId, note: value }));
|
||||
},
|
||||
|
||||
});
|
||||
|
|
|
@ -26,11 +26,16 @@ export const MediaItem: React.FC<{
|
|||
displayMedia === 'show_all',
|
||||
);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const handleImageLoad = useCallback(() => {
|
||||
setLoaded(true);
|
||||
}, [setLoaded]);
|
||||
|
||||
const handleImageError = useCallback(() => {
|
||||
setError(true);
|
||||
}, [setError]);
|
||||
|
||||
const handleMouseEnter = useCallback(
|
||||
(e: React.MouseEvent<HTMLVideoElement>) => {
|
||||
if (e.target instanceof HTMLVideoElement) {
|
||||
|
@ -98,6 +103,7 @@ export const MediaItem: React.FC<{
|
|||
alt={description}
|
||||
lang={lang}
|
||||
onLoad={handleImageLoad}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
|
||||
<div className='media-gallery__item__overlay media-gallery__item__overlay--corner'>
|
||||
|
@ -118,6 +124,7 @@ export const MediaItem: React.FC<{
|
|||
lang={lang}
|
||||
style={{ objectPosition: `${x}% ${y}%` }}
|
||||
onLoad={handleImageLoad}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
);
|
||||
} else if (['video', 'gifv'].includes(type)) {
|
||||
|
@ -173,7 +180,11 @@ export const MediaItem: React.FC<{
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='media-gallery__item media-gallery__item--square'>
|
||||
<div
|
||||
className={classNames('media-gallery__item media-gallery__item--square', {
|
||||
'media-gallery__item--error': error,
|
||||
})}
|
||||
>
|
||||
<Blurhash
|
||||
hash={blurhash}
|
||||
className={classNames('media-gallery__preview', {
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
FollowingCounter,
|
||||
StatusesCounter,
|
||||
} from 'mastodon/components/counters';
|
||||
import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
|
||||
import { getFeaturedHashtagBar } from 'mastodon/components/hashtag_bar';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
|
@ -1020,7 +1021,7 @@ export const AccountHeader: React.FC<{
|
|||
onClickCapture={handleLinkClick}
|
||||
>
|
||||
{account.id !== me && signedIn && (
|
||||
<AccountNoteContainer account={account} />
|
||||
<AccountNoteContainer accountId={accountId} />
|
||||
)}
|
||||
|
||||
{account.note.length > 0 && account.note !== '<p></p>' && (
|
||||
|
@ -1045,11 +1046,12 @@ export const AccountHeader: React.FC<{
|
|||
/>
|
||||
</dt>
|
||||
<dd>
|
||||
{intl.formatDate(account.created_at, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
})}
|
||||
<FormattedDateWrapper
|
||||
value={account.created_at}
|
||||
year='numeric'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
/>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -16,6 +16,7 @@ import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?reac
|
|||
import VolumeOffIcon from '@/material-icons/400-24px/volume_off-fill.svg?react';
|
||||
import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { SpoilerButton } from 'mastodon/components/spoiler_button';
|
||||
import { formatTime, getPointerPosition, fileNameFromURL } from 'mastodon/features/video';
|
||||
|
||||
import { Blurhash } from '../../components/blurhash';
|
||||
|
@ -61,6 +62,7 @@ class Audio extends PureComponent {
|
|||
volume: PropTypes.number,
|
||||
muted: PropTypes.bool,
|
||||
deployPictureInPicture: PropTypes.func,
|
||||
matchedFilters: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -471,19 +473,11 @@ class Audio extends PureComponent {
|
|||
};
|
||||
|
||||
render () {
|
||||
const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props;
|
||||
const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash, matchedFilters } = this.props;
|
||||
const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state;
|
||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||
const muted = this.state.muted || volume === 0;
|
||||
|
||||
let warning;
|
||||
|
||||
if (sensitive) {
|
||||
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
|
||||
} else {
|
||||
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('audio-player', { editable, inactive: !revealed })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), aspectRatio: '16 / 9' }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}>
|
||||
|
||||
|
@ -521,14 +515,7 @@ class Audio extends PureComponent {
|
|||
lang={lang}
|
||||
/>
|
||||
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
|
||||
<button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
|
||||
<span className='spoiler-button__overlay__label'>
|
||||
{warning}
|
||||
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} matchedFilters={matchedFilters} />
|
||||
|
||||
{(revealed || editable) && <img
|
||||
src={this.props.poster}
|
||||
|
|
|
@ -101,6 +101,7 @@ class Bookmarks extends ImmutablePureComponent {
|
|||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
timelineId='bookmarks'
|
||||
/>
|
||||
|
||||
<Helmet>
|
||||
|
|
|
@ -378,6 +378,7 @@ export const LanguageDropdown: React.FC = () => {
|
|||
if (text.length > 20) {
|
||||
debouncedGuess(text, setGuess);
|
||||
} else {
|
||||
debouncedGuess.cancel();
|
||||
setGuess('');
|
||||
}
|
||||
}, [text, setGuess]);
|
||||
|
|
|
@ -101,6 +101,7 @@ class Favourites extends ImmutablePureComponent {
|
|||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
timelineId='favourites'
|
||||
/>
|
||||
|
||||
<Helmet>
|
||||
|
|
|
@ -1,17 +1,13 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
FormattedMessage,
|
||||
FormattedDate,
|
||||
useIntl,
|
||||
defineMessages,
|
||||
} from 'react-intl';
|
||||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { apiGetPrivacyPolicy } from 'mastodon/api/instance';
|
||||
import type { ApiPrivacyPolicyJSON } from 'mastodon/api_types/instance';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
|
||||
import { Skeleton } from 'mastodon/components/skeleton';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -58,7 +54,7 @@ const PrivacyPolicy: React.FC<{
|
|||
date: loading ? (
|
||||
<Skeleton width='10ch' />
|
||||
) : (
|
||||
<FormattedDate
|
||||
<FormattedDateWrapper
|
||||
value={response?.updated_at}
|
||||
year='numeric'
|
||||
month='short'
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { AlertsController } from 'mastodon/components/alerts_controller';
|
||||
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';
|
||||
|
||||
const Compose = () => (
|
||||
<>
|
||||
<ComposeFormContainer autoFocus withoutNavigation />
|
||||
<NotificationsContainer />
|
||||
<AlertsController />
|
||||
<ModalContainer />
|
||||
<LoadingBarContainer className='loading-bar' />
|
||||
</>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import type { CSSProperties } from 'react';
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
|
||||
import { FormattedDate, FormattedMessage } from 'react-intl';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
@ -15,6 +15,8 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re
|
|||
import { AnimatedNumber } from 'mastodon/components/animated_number';
|
||||
import { ContentWarning } from 'mastodon/components/content_warning';
|
||||
import EditedTimestamp from 'mastodon/components/edited_timestamp';
|
||||
import { FilterWarning } from 'mastodon/components/filter_warning';
|
||||
import { FormattedDateWrapper } from 'mastodon/components/formatted_date';
|
||||
import type { StatusLike } from 'mastodon/components/hashtag_bar';
|
||||
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
@ -80,6 +82,7 @@ export const DetailedStatus: React.FC<{
|
|||
}) => {
|
||||
const properStatus = status?.get('reblog') ?? status;
|
||||
const [height, setHeight] = useState(0);
|
||||
const [showDespiteFilter, setShowDespiteFilter] = useState(false);
|
||||
const nodeRef = useRef<HTMLDivElement>();
|
||||
|
||||
const handleOpenVideo = useCallback(
|
||||
|
@ -92,6 +95,10 @@ export const DetailedStatus: React.FC<{
|
|||
[onOpenVideo, status],
|
||||
);
|
||||
|
||||
const handleFilterToggle = useCallback(() => {
|
||||
setShowDespiteFilter(!showDespiteFilter);
|
||||
}, [showDespiteFilter, setShowDespiteFilter]);
|
||||
|
||||
const handleExpandedToggle = useCallback(() => {
|
||||
if (onToggleHidden) onToggleHidden(status);
|
||||
}, [onToggleHidden, status]);
|
||||
|
@ -180,6 +187,7 @@ export const DetailedStatus: React.FC<{
|
|||
onOpenMedia={onOpenMedia}
|
||||
visible={showMedia}
|
||||
onToggleVisibility={onToggleMediaVisibility}
|
||||
matchedFilters={status.get('matched_media_filters')}
|
||||
/>
|
||||
);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
|
@ -206,6 +214,7 @@ export const DetailedStatus: React.FC<{
|
|||
blurhash={attachment.get('blurhash')}
|
||||
height={150}
|
||||
onToggleVisibility={onToggleMediaVisibility}
|
||||
matchedFilters={status.get('matched_media_filters')}
|
||||
/>
|
||||
);
|
||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||
|
@ -229,6 +238,7 @@ export const DetailedStatus: React.FC<{
|
|||
sensitive={status.get('sensitive')}
|
||||
visible={showMedia}
|
||||
onToggleVisibility={onToggleMediaVisibility}
|
||||
matchedFilters={status.get('matched_media_filters')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -369,8 +379,12 @@ export const DetailedStatus: React.FC<{
|
|||
const { statusContentProps, hashtagBar } = getHashtagBarForStatus(
|
||||
status as StatusLike,
|
||||
);
|
||||
|
||||
const matchedFilters = status.get('matched_filters');
|
||||
|
||||
const expanded =
|
||||
!status.get('hidden') || status.get('spoiler_text').length === 0;
|
||||
(!matchedFilters || showDespiteFilter) &&
|
||||
(!status.get('hidden') || status.get('spoiler_text').length === 0);
|
||||
|
||||
const quote = !muted && status.get('quote_id') && (
|
||||
<>
|
||||
|
@ -418,17 +432,26 @@ export const DetailedStatus: React.FC<{
|
|||
)}
|
||||
</Link>
|
||||
|
||||
{status.get('spoiler_text').length > 0 && (
|
||||
<ContentWarning
|
||||
text={
|
||||
status.getIn(['translation', 'spoilerHtml']) ||
|
||||
status.get('spoilerHtml')
|
||||
}
|
||||
expanded={expanded}
|
||||
onClick={handleExpandedToggle}
|
||||
{matchedFilters && (
|
||||
<FilterWarning
|
||||
title={matchedFilters.join(', ')}
|
||||
expanded={showDespiteFilter}
|
||||
onClick={handleFilterToggle}
|
||||
/>
|
||||
)}
|
||||
|
||||
{status.get('spoiler_text').length > 0 &&
|
||||
(!matchedFilters || showDespiteFilter) && (
|
||||
<ContentWarning
|
||||
text={
|
||||
status.getIn(['translation', 'spoilerHtml']) ||
|
||||
status.get('spoilerHtml')
|
||||
}
|
||||
expanded={expanded}
|
||||
onClick={handleExpandedToggle}
|
||||
/>
|
||||
)}
|
||||
|
||||
{expanded && (
|
||||
<>
|
||||
<StatusContent
|
||||
|
@ -452,7 +475,7 @@ export const DetailedStatus: React.FC<{
|
|||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<FormattedDate
|
||||
<FormattedDateWrapper
|
||||
value={new Date(status.get('created_at') as string)}
|
||||
year='numeric'
|
||||
month='short'
|
||||
|
|
|
@ -147,7 +147,7 @@ const makeMapStateToProps = () => {
|
|||
});
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const status = getStatus(state, { id: props.params.statusId });
|
||||
const status = getStatus(state, { id: props.params.statusId, contextType: 'detailed' });
|
||||
|
||||
let ancestorsIds = ImmutableList();
|
||||
let descendantsIds = ImmutableList();
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { LoadingBar } from 'react-redux-loading-bar';
|
||||
|
||||
import ZoomableImage from './zoomable_image';
|
||||
|
||||
export default class ImageLoader extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
alt: PropTypes.string,
|
||||
lang: PropTypes.string,
|
||||
src: PropTypes.string.isRequired,
|
||||
previewSrc: PropTypes.string,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
onClick: PropTypes.func,
|
||||
zoomedIn: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
alt: '',
|
||||
lang: '',
|
||||
width: null,
|
||||
height: null,
|
||||
};
|
||||
|
||||
state = {
|
||||
loading: true,
|
||||
error: false,
|
||||
width: null,
|
||||
};
|
||||
|
||||
removers = [];
|
||||
canvas = null;
|
||||
|
||||
get canvasContext() {
|
||||
if (!this.canvas) {
|
||||
return null;
|
||||
}
|
||||
this._canvasContext = this._canvasContext || this.canvas.getContext('2d');
|
||||
return this._canvasContext;
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.loadImage(this.props);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
if (this.props.src !== nextProps.src) {
|
||||
this.loadImage(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.removeEventListeners();
|
||||
}
|
||||
|
||||
loadImage (props) {
|
||||
this.removeEventListeners();
|
||||
this.setState({ loading: true, error: false });
|
||||
Promise.all([
|
||||
props.previewSrc && this.loadPreviewCanvas(props),
|
||||
this.hasSize() && this.loadOriginalImage(props),
|
||||
].filter(Boolean))
|
||||
.then(() => {
|
||||
this.setState({ loading: false, error: false });
|
||||
this.clearPreviewCanvas();
|
||||
})
|
||||
.catch(() => this.setState({ loading: false, error: true }));
|
||||
}
|
||||
|
||||
loadPreviewCanvas = ({ previewSrc, width, height }) => new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
const removeEventListeners = () => {
|
||||
image.removeEventListener('error', handleError);
|
||||
image.removeEventListener('load', handleLoad);
|
||||
};
|
||||
const handleError = () => {
|
||||
removeEventListeners();
|
||||
reject();
|
||||
};
|
||||
const handleLoad = () => {
|
||||
removeEventListeners();
|
||||
this.canvasContext.drawImage(image, 0, 0, width, height);
|
||||
resolve();
|
||||
};
|
||||
image.addEventListener('error', handleError);
|
||||
image.addEventListener('load', handleLoad);
|
||||
image.src = previewSrc;
|
||||
this.removers.push(removeEventListeners);
|
||||
});
|
||||
|
||||
clearPreviewCanvas () {
|
||||
const { width, height } = this.canvas;
|
||||
this.canvasContext.clearRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
loadOriginalImage = ({ src }) => new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
const removeEventListeners = () => {
|
||||
image.removeEventListener('error', handleError);
|
||||
image.removeEventListener('load', handleLoad);
|
||||
};
|
||||
const handleError = () => {
|
||||
removeEventListeners();
|
||||
reject();
|
||||
};
|
||||
const handleLoad = () => {
|
||||
removeEventListeners();
|
||||
resolve();
|
||||
};
|
||||
image.addEventListener('error', handleError);
|
||||
image.addEventListener('load', handleLoad);
|
||||
image.src = src;
|
||||
this.removers.push(removeEventListeners);
|
||||
});
|
||||
|
||||
removeEventListeners () {
|
||||
this.removers.forEach(listeners => listeners());
|
||||
this.removers = [];
|
||||
}
|
||||
|
||||
hasSize () {
|
||||
const { width, height } = this.props;
|
||||
return typeof width === 'number' && typeof height === 'number';
|
||||
}
|
||||
|
||||
setCanvasRef = c => {
|
||||
this.canvas = c;
|
||||
if (c) this.setState({ width: c.offsetWidth });
|
||||
};
|
||||
|
||||
render () {
|
||||
const { alt, lang, src, width, height, onClick, zoomedIn } = this.props;
|
||||
const { loading } = this.state;
|
||||
|
||||
const className = classNames('image-loader', {
|
||||
'image-loader--loading': loading,
|
||||
'image-loader--amorphous': !this.hasSize(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className='loading-bar__container' style={{ width: this.state.width || width }}>
|
||||
<LoadingBar className='loading-bar' loading={1} />
|
||||
</div>
|
||||
|
||||
<canvas
|
||||
className='image-loader__preview-canvas'
|
||||
ref={this.setCanvasRef}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ZoomableImage
|
||||
alt={alt}
|
||||
lang={lang}
|
||||
src={src}
|
||||
onClick={onClick}
|
||||
width={width}
|
||||
height={height}
|
||||
zoomedIn={zoomedIn}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
|
||||
import ImageLoader from './image_loader';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
class ImageModal extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
src: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
state = {
|
||||
navigationHidden: false,
|
||||
};
|
||||
|
||||
toggleNavigation = () => {
|
||||
this.setState(prevState => ({
|
||||
navigationHidden: !prevState.navigationHidden,
|
||||
}));
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, src, alt, onClose } = this.props;
|
||||
const { navigationHidden } = this.state;
|
||||
|
||||
const navigationClassName = classNames('media-modal__navigation', {
|
||||
'media-modal__navigation--hidden': navigationHidden,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal media-modal'>
|
||||
<div className='media-modal__closer' role='presentation' onClick={onClose} >
|
||||
<ImageLoader
|
||||
src={src}
|
||||
width={400}
|
||||
height={400}
|
||||
alt={alt}
|
||||
onClick={this.toggleNavigation}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={navigationClassName}>
|
||||
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} size={40} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(ImageModal);
|
|
@ -0,0 +1,61 @@
|
|||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
|
||||
import { ZoomableImage } from './zoomable_image';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
export const ImageModal: React.FC<{
|
||||
src: string;
|
||||
alt: string;
|
||||
onClose: () => void;
|
||||
}> = ({ src, alt, onClose }) => {
|
||||
const intl = useIntl();
|
||||
const [navigationHidden, setNavigationHidden] = useState(false);
|
||||
|
||||
const toggleNavigation = useCallback(() => {
|
||||
setNavigationHidden((prevState) => !prevState);
|
||||
}, [setNavigationHidden]);
|
||||
|
||||
const navigationClassName = classNames('media-modal__navigation', {
|
||||
'media-modal__navigation--hidden': navigationHidden,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal media-modal'>
|
||||
<div
|
||||
className='media-modal__closer'
|
||||
role='presentation'
|
||||
onClick={onClose}
|
||||
>
|
||||
<ZoomableImage
|
||||
src={src}
|
||||
width={400}
|
||||
height={400}
|
||||
alt={alt}
|
||||
onClick={toggleNavigation}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={navigationClassName}>
|
||||
<div className='media-modal__buttons'>
|
||||
<IconButton
|
||||
className='media-modal__close'
|
||||
title={intl.formatMessage(messages.close)}
|
||||
icon='times'
|
||||
iconComponent={CloseIcon}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -22,7 +22,7 @@ import Footer from 'mastodon/features/picture_in_picture/components/footer';
|
|||
import Video from 'mastodon/features/video';
|
||||
import { disableSwiping } from 'mastodon/initial_state';
|
||||
|
||||
import ImageLoader from './image_loader';
|
||||
import { ZoomableImage } from './zoomable_image';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
|
@ -59,6 +59,12 @@ class MediaModal extends ImmutablePureComponent {
|
|||
}));
|
||||
};
|
||||
|
||||
handleZoomChange = (zoomedIn) => {
|
||||
this.setState({
|
||||
zoomedIn,
|
||||
});
|
||||
};
|
||||
|
||||
handleSwipe = (index) => {
|
||||
this.setState({
|
||||
index: index % this.props.media.size,
|
||||
|
@ -165,23 +171,26 @@ class MediaModal extends ImmutablePureComponent {
|
|||
const leftNav = media.size > 1 && <button tabIndex={0} className='media-modal__nav media-modal__nav--left' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>;
|
||||
const rightNav = media.size > 1 && <button tabIndex={0} className='media-modal__nav media-modal__nav--right' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>;
|
||||
|
||||
const content = media.map((image) => {
|
||||
const content = media.map((image, idx) => {
|
||||
const width = image.getIn(['meta', 'original', 'width']) || null;
|
||||
const height = image.getIn(['meta', 'original', 'height']) || null;
|
||||
const description = image.getIn(['translation', 'description']) || image.get('description');
|
||||
|
||||
if (image.get('type') === 'image') {
|
||||
return (
|
||||
<ImageLoader
|
||||
previewSrc={image.get('preview_url')}
|
||||
<ZoomableImage
|
||||
src={image.get('url')}
|
||||
blurhash={image.get('blurhash')}
|
||||
width={width}
|
||||
height={height}
|
||||
alt={description}
|
||||
lang={lang}
|
||||
key={image.get('url')}
|
||||
onClick={this.handleToggleNavigation}
|
||||
zoomedIn={zoomedIn}
|
||||
onDoubleClick={this.handleZoomClick}
|
||||
onClose={onClose}
|
||||
onZoomChange={this.handleZoomChange}
|
||||
zoomedIn={zoomedIn && idx === index}
|
||||
/>
|
||||
);
|
||||
} else if (image.get('type') === 'video') {
|
||||
|
@ -262,7 +271,7 @@ class MediaModal extends ImmutablePureComponent {
|
|||
onChangeIndex={this.handleSwipe}
|
||||
onTransitionEnd={this.handleTransitionEnd}
|
||||
index={index}
|
||||
disabled={disableSwiping}
|
||||
disabled={disableSwiping || zoomedIn}
|
||||
>
|
||||
{content}
|
||||
</ReactSwipeableViews>
|
||||
|
|
|
@ -45,7 +45,7 @@ import {
|
|||
ConfirmFollowToListModal,
|
||||
ConfirmMissingAltTextModal,
|
||||
} from './confirmation_modals';
|
||||
import ImageModal from './image_modal';
|
||||
import { ImageModal } from './image_modal';
|
||||
import MediaModal from './media_modal';
|
||||
import { ModalPlaceholder } from './modal_placeholder';
|
||||
import VideoModal from './video_modal';
|
||||
|
|
|
@ -1,402 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
const MIN_SCALE = 1;
|
||||
const MAX_SCALE = 4;
|
||||
const NAV_BAR_HEIGHT = 66;
|
||||
|
||||
const getMidpoint = (p1, p2) => ({
|
||||
x: (p1.clientX + p2.clientX) / 2,
|
||||
y: (p1.clientY + p2.clientY) / 2,
|
||||
});
|
||||
|
||||
const getDistance = (p1, p2) =>
|
||||
Math.sqrt(Math.pow(p1.clientX - p2.clientX, 2) + Math.pow(p1.clientY - p2.clientY, 2));
|
||||
|
||||
const clamp = (min, max, value) => Math.min(max, Math.max(min, value));
|
||||
|
||||
// Normalizing mousewheel speed across browsers
|
||||
// copy from: https://github.com/facebookarchive/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js
|
||||
const normalizeWheel = event => {
|
||||
// Reasonable defaults
|
||||
const PIXEL_STEP = 10;
|
||||
const LINE_HEIGHT = 40;
|
||||
const PAGE_HEIGHT = 800;
|
||||
|
||||
let sX = 0,
|
||||
sY = 0, // spinX, spinY
|
||||
pX = 0,
|
||||
pY = 0; // pixelX, pixelY
|
||||
|
||||
// Legacy
|
||||
if ('detail' in event) {
|
||||
sY = event.detail;
|
||||
}
|
||||
if ('wheelDelta' in event) {
|
||||
sY = -event.wheelDelta / 120;
|
||||
}
|
||||
if ('wheelDeltaY' in event) {
|
||||
sY = -event.wheelDeltaY / 120;
|
||||
}
|
||||
if ('wheelDeltaX' in event) {
|
||||
sX = -event.wheelDeltaX / 120;
|
||||
}
|
||||
|
||||
// side scrolling on FF with DOMMouseScroll
|
||||
if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) {
|
||||
sX = sY;
|
||||
sY = 0;
|
||||
}
|
||||
|
||||
pX = sX * PIXEL_STEP;
|
||||
pY = sY * PIXEL_STEP;
|
||||
|
||||
if ('deltaY' in event) {
|
||||
pY = event.deltaY;
|
||||
}
|
||||
if ('deltaX' in event) {
|
||||
pX = event.deltaX;
|
||||
}
|
||||
|
||||
if ((pX || pY) && event.deltaMode) {
|
||||
if (event.deltaMode === 1) { // delta in LINE units
|
||||
pX *= LINE_HEIGHT;
|
||||
pY *= LINE_HEIGHT;
|
||||
} else { // delta in PAGE units
|
||||
pX *= PAGE_HEIGHT;
|
||||
pY *= PAGE_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall-back if spin cannot be determined
|
||||
if (pX && !sX) {
|
||||
sX = (pX < 1) ? -1 : 1;
|
||||
}
|
||||
if (pY && !sY) {
|
||||
sY = (pY < 1) ? -1 : 1;
|
||||
}
|
||||
|
||||
return {
|
||||
spinX: sX,
|
||||
spinY: sY,
|
||||
pixelX: pX,
|
||||
pixelY: pY,
|
||||
};
|
||||
};
|
||||
|
||||
class ZoomableImage extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
alt: PropTypes.string,
|
||||
lang: PropTypes.string,
|
||||
src: PropTypes.string.isRequired,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
onClick: PropTypes.func,
|
||||
zoomedIn: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
alt: '',
|
||||
lang: '',
|
||||
width: null,
|
||||
height: null,
|
||||
};
|
||||
|
||||
state = {
|
||||
scale: MIN_SCALE,
|
||||
zoomMatrix: {
|
||||
type: null, // 'width' 'height'
|
||||
fullScreen: null, // bool
|
||||
rate: null, // full screen scale rate
|
||||
clientWidth: null,
|
||||
clientHeight: null,
|
||||
offsetWidth: null,
|
||||
offsetHeight: null,
|
||||
clientHeightFixed: null,
|
||||
scrollTop: null,
|
||||
scrollLeft: null,
|
||||
translateX: null,
|
||||
translateY: null,
|
||||
},
|
||||
dragPosition: { top: 0, left: 0, x: 0, y: 0 },
|
||||
dragged: false,
|
||||
lockScroll: { x: 0, y: 0 },
|
||||
lockTranslate: { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
removers = [];
|
||||
container = null;
|
||||
image = null;
|
||||
lastTouchEndTime = 0;
|
||||
lastDistance = 0;
|
||||
|
||||
componentDidMount () {
|
||||
let handler = this.handleTouchStart;
|
||||
this.container.addEventListener('touchstart', handler);
|
||||
this.removers.push(() => this.container.removeEventListener('touchstart', handler));
|
||||
handler = this.handleTouchMove;
|
||||
// on Chrome 56+, touch event listeners will default to passive
|
||||
// https://www.chromestatus.com/features/5093566007214080
|
||||
this.container.addEventListener('touchmove', handler, { passive: false });
|
||||
this.removers.push(() => this.container.removeEventListener('touchend', handler));
|
||||
|
||||
handler = this.mouseDownHandler;
|
||||
this.container.addEventListener('mousedown', handler);
|
||||
this.removers.push(() => this.container.removeEventListener('mousedown', handler));
|
||||
|
||||
handler = this.mouseWheelHandler;
|
||||
this.container.addEventListener('wheel', handler);
|
||||
this.removers.push(() => this.container.removeEventListener('wheel', handler));
|
||||
// Old Chrome
|
||||
this.container.addEventListener('mousewheel', handler);
|
||||
this.removers.push(() => this.container.removeEventListener('mousewheel', handler));
|
||||
// Old Firefox
|
||||
this.container.addEventListener('DOMMouseScroll', handler);
|
||||
this.removers.push(() => this.container.removeEventListener('DOMMouseScroll', handler));
|
||||
|
||||
this._initZoomMatrix();
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._removeEventListeners();
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.zoomedIn !== this.props.zoomedIn) {
|
||||
this._toggleZoom();
|
||||
}
|
||||
}
|
||||
|
||||
_removeEventListeners () {
|
||||
this.removers.forEach(listeners => listeners());
|
||||
this.removers = [];
|
||||
}
|
||||
|
||||
mouseWheelHandler = e => {
|
||||
e.preventDefault();
|
||||
|
||||
const event = normalizeWheel(e);
|
||||
|
||||
if (this.state.zoomMatrix.type === 'width') {
|
||||
// full width, scroll vertical
|
||||
this.container.scrollTop = Math.max(this.container.scrollTop + event.pixelY, this.state.lockScroll.y);
|
||||
} else {
|
||||
// full height, scroll horizontal
|
||||
this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelY, this.state.lockScroll.x);
|
||||
}
|
||||
|
||||
// lock horizontal scroll
|
||||
this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelX, this.state.lockScroll.x);
|
||||
};
|
||||
|
||||
mouseDownHandler = e => {
|
||||
this.setState({ dragPosition: {
|
||||
left: this.container.scrollLeft,
|
||||
top: this.container.scrollTop,
|
||||
// Get the current mouse position
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
} });
|
||||
|
||||
this.image.addEventListener('mousemove', this.mouseMoveHandler);
|
||||
this.image.addEventListener('mouseup', this.mouseUpHandler);
|
||||
};
|
||||
|
||||
mouseMoveHandler = e => {
|
||||
const dx = e.clientX - this.state.dragPosition.x;
|
||||
const dy = e.clientY - this.state.dragPosition.y;
|
||||
|
||||
this.container.scrollLeft = Math.max(this.state.dragPosition.left - dx, this.state.lockScroll.x);
|
||||
this.container.scrollTop = Math.max(this.state.dragPosition.top - dy, this.state.lockScroll.y);
|
||||
|
||||
this.setState({ dragged: true });
|
||||
};
|
||||
|
||||
mouseUpHandler = () => {
|
||||
this.image.removeEventListener('mousemove', this.mouseMoveHandler);
|
||||
this.image.removeEventListener('mouseup', this.mouseUpHandler);
|
||||
};
|
||||
|
||||
handleTouchStart = e => {
|
||||
if (e.touches.length !== 2) return;
|
||||
|
||||
this.lastDistance = getDistance(...e.touches);
|
||||
};
|
||||
|
||||
handleTouchMove = e => {
|
||||
const { scrollTop, scrollHeight, clientHeight } = this.container;
|
||||
if (e.touches.length === 1 && scrollTop !== scrollHeight - clientHeight) {
|
||||
// prevent propagating event to MediaModal
|
||||
e.stopPropagation();
|
||||
return;
|
||||
}
|
||||
if (e.touches.length !== 2) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const distance = getDistance(...e.touches);
|
||||
const midpoint = getMidpoint(...e.touches);
|
||||
const _MAX_SCALE = Math.max(MAX_SCALE, this.state.zoomMatrix.rate);
|
||||
const scale = clamp(MIN_SCALE, _MAX_SCALE, this.state.scale * distance / this.lastDistance);
|
||||
|
||||
this._zoom(scale, midpoint);
|
||||
|
||||
this.lastMidpoint = midpoint;
|
||||
this.lastDistance = distance;
|
||||
};
|
||||
|
||||
_zoom(nextScale, midpoint) {
|
||||
const { scale, zoomMatrix } = this.state;
|
||||
const { scrollLeft, scrollTop } = this.container;
|
||||
|
||||
// math memo:
|
||||
// x = (scrollLeft + midpoint.x) / scrollWidth
|
||||
// x' = (nextScrollLeft + midpoint.x) / nextScrollWidth
|
||||
// scrollWidth = clientWidth * scale
|
||||
// scrollWidth' = clientWidth * nextScale
|
||||
// Solve x = x' for nextScrollLeft
|
||||
const nextScrollLeft = (scrollLeft + midpoint.x) * nextScale / scale - midpoint.x;
|
||||
const nextScrollTop = (scrollTop + midpoint.y) * nextScale / scale - midpoint.y;
|
||||
|
||||
this.setState({ scale: nextScale }, () => {
|
||||
this.container.scrollLeft = nextScrollLeft;
|
||||
this.container.scrollTop = nextScrollTop;
|
||||
// reset the translateX/Y constantly
|
||||
if (nextScale < zoomMatrix.rate) {
|
||||
this.setState({
|
||||
lockTranslate: {
|
||||
x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)),
|
||||
y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY * ((nextScale - MIN_SCALE) / (zoomMatrix.rate - MIN_SCALE)),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
handleClick = e => {
|
||||
// don't propagate event to MediaModal
|
||||
e.stopPropagation();
|
||||
const dragged = this.state.dragged;
|
||||
this.setState({ dragged: false });
|
||||
if (dragged) return;
|
||||
const handler = this.props.onClick;
|
||||
if (handler) handler();
|
||||
};
|
||||
|
||||
handleMouseDown = e => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
_initZoomMatrix = () => {
|
||||
const { width, height } = this.props;
|
||||
const { clientWidth, clientHeight } = this.container;
|
||||
const { offsetWidth, offsetHeight } = this.image;
|
||||
const clientHeightFixed = clientHeight - NAV_BAR_HEIGHT;
|
||||
|
||||
const type = width / height < clientWidth / clientHeightFixed ? 'width' : 'height';
|
||||
const fullScreen = type === 'width' ? width > clientWidth : height > clientHeightFixed;
|
||||
const rate = type === 'width' ? Math.min(clientWidth, width) / offsetWidth : Math.min(clientHeightFixed, height) / offsetHeight;
|
||||
const scrollTop = type === 'width' ? (clientHeight - offsetHeight) / 2 - NAV_BAR_HEIGHT : (clientHeightFixed - offsetHeight) / 2;
|
||||
const scrollLeft = (clientWidth - offsetWidth) / 2;
|
||||
const translateX = type === 'width' ? (width - offsetWidth) / (2 * rate) : 0;
|
||||
const translateY = type === 'height' ? (height - offsetHeight) / (2 * rate) : 0;
|
||||
|
||||
this.setState({
|
||||
zoomMatrix: {
|
||||
type: type,
|
||||
fullScreen: fullScreen,
|
||||
rate: rate,
|
||||
clientWidth: clientWidth,
|
||||
clientHeight: clientHeight,
|
||||
offsetWidth: offsetWidth,
|
||||
offsetHeight: offsetHeight,
|
||||
clientHeightFixed: clientHeightFixed,
|
||||
scrollTop: scrollTop,
|
||||
scrollLeft: scrollLeft,
|
||||
translateX: translateX,
|
||||
translateY: translateY,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
_toggleZoom () {
|
||||
const { scale, zoomMatrix } = this.state;
|
||||
|
||||
if ( scale >= zoomMatrix.rate ) {
|
||||
this.setState({
|
||||
scale: MIN_SCALE,
|
||||
lockScroll: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
lockTranslate: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
}, () => {
|
||||
this.container.scrollLeft = 0;
|
||||
this.container.scrollTop = 0;
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
scale: zoomMatrix.rate,
|
||||
lockScroll: {
|
||||
x: zoomMatrix.scrollLeft,
|
||||
y: zoomMatrix.scrollTop,
|
||||
},
|
||||
lockTranslate: {
|
||||
x: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateX,
|
||||
y: zoomMatrix.fullScreen ? 0 : zoomMatrix.translateY,
|
||||
},
|
||||
}, () => {
|
||||
this.container.scrollLeft = zoomMatrix.scrollLeft;
|
||||
this.container.scrollTop = zoomMatrix.scrollTop;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setContainerRef = c => {
|
||||
this.container = c;
|
||||
};
|
||||
|
||||
setImageRef = c => {
|
||||
this.image = c;
|
||||
};
|
||||
|
||||
render () {
|
||||
const { alt, lang, src, width, height } = this.props;
|
||||
const { scale, lockTranslate, dragged } = this.state;
|
||||
const overflow = scale === MIN_SCALE ? 'hidden' : 'scroll';
|
||||
const cursor = scale === MIN_SCALE ? null : (dragged ? 'grabbing' : 'grab');
|
||||
|
||||
return (
|
||||
<div
|
||||
className='zoomable-image'
|
||||
ref={this.setContainerRef}
|
||||
style={{ overflow, cursor, userSelect: 'none' }}
|
||||
>
|
||||
<img
|
||||
role='presentation'
|
||||
ref={this.setImageRef}
|
||||
alt={alt}
|
||||
title={alt}
|
||||
lang={lang}
|
||||
src={src}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{
|
||||
transform: `scale(${scale}) translate(-${lockTranslate.x}px, -${lockTranslate.y}px)`,
|
||||
transformOrigin: '0 0',
|
||||
}}
|
||||
draggable={false}
|
||||
onClick={this.handleClick}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ZoomableImage;
|
|
@ -0,0 +1,319 @@
|
|||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useSpring, animated, config } from '@react-spring/web';
|
||||
import { createUseGesture, dragAction, pinchAction } from '@use-gesture/react';
|
||||
|
||||
import { Blurhash } from 'mastodon/components/blurhash';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
|
||||
const MIN_SCALE = 1;
|
||||
const MAX_SCALE = 4;
|
||||
const DOUBLE_CLICK_THRESHOLD = 250;
|
||||
|
||||
interface ZoomMatrix {
|
||||
containerWidth: number;
|
||||
containerHeight: number;
|
||||
imageWidth: number;
|
||||
imageHeight: number;
|
||||
initialScale: number;
|
||||
}
|
||||
|
||||
const createZoomMatrix = (
|
||||
container: HTMLElement,
|
||||
image: HTMLImageElement,
|
||||
fullWidth: number,
|
||||
fullHeight: number,
|
||||
): ZoomMatrix => {
|
||||
const { clientWidth, clientHeight } = container;
|
||||
const { offsetWidth, offsetHeight } = image;
|
||||
|
||||
const type =
|
||||
fullWidth / fullHeight < clientWidth / clientHeight ? 'width' : 'height';
|
||||
|
||||
const initialScale =
|
||||
type === 'width'
|
||||
? Math.min(clientWidth, fullWidth) / offsetWidth
|
||||
: Math.min(clientHeight, fullHeight) / offsetHeight;
|
||||
|
||||
return {
|
||||
containerWidth: clientWidth,
|
||||
containerHeight: clientHeight,
|
||||
imageWidth: offsetWidth,
|
||||
imageHeight: offsetHeight,
|
||||
initialScale,
|
||||
};
|
||||
};
|
||||
|
||||
const useGesture = createUseGesture([dragAction, pinchAction]);
|
||||
|
||||
const getBounds = (zoomMatrix: ZoomMatrix | null, scale: number) => {
|
||||
if (!zoomMatrix || scale === MIN_SCALE) {
|
||||
return {
|
||||
left: -Infinity,
|
||||
right: Infinity,
|
||||
top: -Infinity,
|
||||
bottom: Infinity,
|
||||
};
|
||||
}
|
||||
|
||||
const { containerWidth, containerHeight, imageWidth, imageHeight } =
|
||||
zoomMatrix;
|
||||
|
||||
const bounds = {
|
||||
left: -Math.max(imageWidth * scale - containerWidth, 0) / 2,
|
||||
right: Math.max(imageWidth * scale - containerWidth, 0) / 2,
|
||||
top: -Math.max(imageHeight * scale - containerHeight, 0) / 2,
|
||||
bottom: Math.max(imageHeight * scale - containerHeight, 0) / 2,
|
||||
};
|
||||
|
||||
return bounds;
|
||||
};
|
||||
|
||||
interface ZoomableImageProps {
|
||||
alt?: string;
|
||||
lang?: string;
|
||||
src: string;
|
||||
width: number;
|
||||
height: number;
|
||||
onClick?: () => void;
|
||||
onDoubleClick?: () => void;
|
||||
onClose?: () => void;
|
||||
onZoomChange?: (zoomedIn: boolean) => void;
|
||||
zoomedIn?: boolean;
|
||||
blurhash?: string;
|
||||
}
|
||||
|
||||
export const ZoomableImage: React.FC<ZoomableImageProps> = ({
|
||||
alt = '',
|
||||
lang = '',
|
||||
src,
|
||||
width,
|
||||
height,
|
||||
onClick,
|
||||
onDoubleClick,
|
||||
onClose,
|
||||
onZoomChange,
|
||||
zoomedIn,
|
||||
blurhash,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const handler = (e: Event) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
document.addEventListener('gesturestart', handler);
|
||||
document.addEventListener('gesturechange', handler);
|
||||
document.addEventListener('gestureend', handler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('gesturestart', handler);
|
||||
document.removeEventListener('gesturechange', handler);
|
||||
document.removeEventListener('gestureend', handler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [dragging, setDragging] = useState(false);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
const doubleClickTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>();
|
||||
const zoomMatrixRef = useRef<ZoomMatrix | null>(null);
|
||||
|
||||
const [style, api] = useSpring(() => ({
|
||||
x: 0,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
onRest: {
|
||||
scale({ value }) {
|
||||
if (!onZoomChange) {
|
||||
return;
|
||||
}
|
||||
if (value === MIN_SCALE) {
|
||||
onZoomChange(false);
|
||||
} else {
|
||||
onZoomChange(true);
|
||||
}
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
useGesture(
|
||||
{
|
||||
onDrag({
|
||||
pinching,
|
||||
cancel,
|
||||
active,
|
||||
last,
|
||||
offset: [x, y],
|
||||
velocity: [, vy],
|
||||
direction: [, dy],
|
||||
tap,
|
||||
}) {
|
||||
if (tap) {
|
||||
if (!doubleClickTimeoutRef.current) {
|
||||
doubleClickTimeoutRef.current = setTimeout(() => {
|
||||
onClick?.();
|
||||
doubleClickTimeoutRef.current = null;
|
||||
}, DOUBLE_CLICK_THRESHOLD);
|
||||
} else {
|
||||
clearTimeout(doubleClickTimeoutRef.current);
|
||||
doubleClickTimeoutRef.current = null;
|
||||
onDoubleClick?.();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!zoomedIn) {
|
||||
// Swipe up/down to dismiss parent
|
||||
if (last) {
|
||||
if ((vy > 0.5 && dy !== 0) || Math.abs(y) > 150) {
|
||||
onClose?.();
|
||||
}
|
||||
|
||||
void api.start({ y: 0, config: config.wobbly });
|
||||
return;
|
||||
} else if (dy !== 0) {
|
||||
void api.start({ y, immediate: true });
|
||||
return;
|
||||
}
|
||||
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pinching) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (active) {
|
||||
setDragging(true);
|
||||
} else {
|
||||
setDragging(false);
|
||||
}
|
||||
|
||||
void api.start({ x, y });
|
||||
},
|
||||
|
||||
onPinch({ origin: [ox, oy], first, movement: [ms], offset: [s], memo }) {
|
||||
if (!imageRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (first) {
|
||||
const { width, height, x, y } =
|
||||
imageRef.current.getBoundingClientRect();
|
||||
const tx = ox - (x + width / 2);
|
||||
const ty = oy - (y + height / 2);
|
||||
|
||||
memo = [style.x.get(), style.y.get(), tx, ty];
|
||||
}
|
||||
|
||||
const x = memo[0] - (ms - 1) * memo[2]; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||
const y = memo[1] - (ms - 1) * memo[3]; // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
||||
|
||||
void api.start({ scale: s, x, y });
|
||||
|
||||
return memo as [number, number, number, number];
|
||||
},
|
||||
},
|
||||
{
|
||||
target: imageRef,
|
||||
drag: {
|
||||
from: () => [style.x.get(), style.y.get()],
|
||||
filterTaps: true,
|
||||
bounds: () => getBounds(zoomMatrixRef.current, style.scale.get()),
|
||||
rubberband: true,
|
||||
},
|
||||
pinch: {
|
||||
scaleBounds: {
|
||||
min: MIN_SCALE,
|
||||
max: MAX_SCALE,
|
||||
},
|
||||
rubberband: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loaded || !containerRef.current || !imageRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
zoomMatrixRef.current = createZoomMatrix(
|
||||
containerRef.current,
|
||||
imageRef.current,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
|
||||
if (!zoomedIn) {
|
||||
void api.start({ scale: MIN_SCALE, x: 0, y: 0 });
|
||||
} else if (style.scale.get() === MIN_SCALE) {
|
||||
void api.start({ scale: zoomMatrixRef.current.initialScale, x: 0, y: 0 });
|
||||
}
|
||||
}, [api, style.scale, zoomedIn, width, height, loaded]);
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||
// This handler exists to cancel the onClick handler on the media modal which would
|
||||
// otherwise close the modal. It cannot be used for actual click handling because
|
||||
// we don't know if the user is about to pan the image or not.
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
const handleLoad = useCallback(() => {
|
||||
setLoaded(true);
|
||||
}, [setLoaded]);
|
||||
|
||||
const handleError = useCallback(() => {
|
||||
setError(true);
|
||||
}, [setError]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('zoomable-image', {
|
||||
'zoomable-image--zoomed-in': zoomedIn,
|
||||
'zoomable-image--error': error,
|
||||
'zoomable-image--dragging': dragging,
|
||||
})}
|
||||
ref={containerRef}
|
||||
>
|
||||
{!loaded && blurhash && (
|
||||
<div
|
||||
className='zoomable-image__preview'
|
||||
style={{
|
||||
aspectRatio: `${width}/${height}`,
|
||||
height: `min(${height}px, 100%)`,
|
||||
}}
|
||||
>
|
||||
<Blurhash hash={blurhash} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<animated.img
|
||||
style={style}
|
||||
role='presentation'
|
||||
ref={imageRef}
|
||||
alt={alt}
|
||||
title={alt}
|
||||
lang={lang}
|
||||
src={src}
|
||||
width={width}
|
||||
height={height}
|
||||
draggable={false}
|
||||
onLoad={handleLoad}
|
||||
onError={handleError}
|
||||
onClickCapture={handleClick}
|
||||
/>
|
||||
|
||||
{!loaded && !error && <LoadingIndicator />}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,20 +0,0 @@
|
|||
import { injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { NotificationStack } from 'react-notification';
|
||||
|
||||
import { dismissAlert } from 'mastodon/actions/alerts';
|
||||
import { getAlerts } from 'mastodon/selectors';
|
||||
|
||||
const mapStateToProps = (state, { intl }) => ({
|
||||
notifications: getAlerts(state, { intl }),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
onDismiss (alert) {
|
||||
dispatch(dismissAlert(alert));
|
||||
},
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));
|
|
@ -15,6 +15,7 @@ import { focusApp, unfocusApp, changeLayout } from 'mastodon/actions/app';
|
|||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
|
||||
import { fetchNotifications } from 'mastodon/actions/notification_groups';
|
||||
import { INTRODUCTION_VERSION } from 'mastodon/actions/onboarding';
|
||||
import { AlertsController } from 'mastodon/components/alerts_controller';
|
||||
import { HoverCardController } from 'mastodon/components/hover_card_controller';
|
||||
import { PictureInPicture } from 'mastodon/features/picture_in_picture';
|
||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||
|
@ -33,7 +34,6 @@ import UploadArea from './components/upload_area';
|
|||
import ColumnsAreaContainer from './containers/columns_area_container';
|
||||
import LoadingBarContainer from './containers/loading_bar_container';
|
||||
import ModalContainer from './containers/modal_container';
|
||||
import NotificationsContainer from './containers/notifications_container';
|
||||
import {
|
||||
Compose,
|
||||
Status,
|
||||
|
@ -656,7 +656,7 @@ class UI extends PureComponent {
|
|||
</SwitchingColumnsArea>
|
||||
|
||||
{layout !== 'mobile' && <PictureInPicture />}
|
||||
<NotificationsContainer />
|
||||
<AlertsController />
|
||||
{!disableHoverCards && <HoverCardController />}
|
||||
<LoadingBarContainer className='loading-bar' />
|
||||
<ModalContainer />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -19,6 +19,7 @@ import VolumeOffIcon from '@/material-icons/400-24px/volume_off-fill.svg?react';
|
|||
import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react';
|
||||
import { Blurhash } from 'mastodon/components/blurhash';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { SpoilerButton } from 'mastodon/components/spoiler_button';
|
||||
import { playerSettings } from 'mastodon/settings';
|
||||
|
||||
import { displayMedia, useBlurhash } from '../../initial_state';
|
||||
|
@ -135,6 +136,7 @@ class Video extends PureComponent {
|
|||
muted: PropTypes.bool,
|
||||
componentIndex: PropTypes.number,
|
||||
autoFocus: PropTypes.bool,
|
||||
matchedFilters: PropTypes.arrayOf(PropTypes.string),
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -534,7 +536,7 @@ class Video extends PureComponent {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { preview, src, aspectRatio, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus } = this.props;
|
||||
const { preview, src, aspectRatio, onOpenVideo, onCloseVideo, intl, alt, lang, detailed, sensitive, editable, blurhash, autoFocus, matchedFilters } = this.props;
|
||||
const { currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, revealed } = this.state;
|
||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||
const muted = this.state.muted || volume === 0;
|
||||
|
@ -549,14 +551,6 @@ class Video extends PureComponent {
|
|||
preload = 'none';
|
||||
}
|
||||
|
||||
let warning;
|
||||
|
||||
if (sensitive) {
|
||||
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />;
|
||||
} else {
|
||||
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />;
|
||||
}
|
||||
|
||||
// The outer wrapper is necessary to avoid reflowing the layout when going into full screen
|
||||
return (
|
||||
<div style={{ aspectRatio }}>
|
||||
|
@ -599,14 +593,7 @@ class Video extends PureComponent {
|
|||
style={{ width: '100%' }}
|
||||
/>}
|
||||
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}>
|
||||
<button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}>
|
||||
<span className='spoiler-button__overlay__label'>
|
||||
{warning}
|
||||
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<SpoilerButton hidden={revealed || editable} sensitive={sensitive} onClick={this.toggleReveal} matchedFilters={matchedFilters} />
|
||||
|
||||
<div className={classNames('video-player__controls', { active: paused || hovered })}>
|
||||
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}>
|
||||
|
|
|
@ -81,9 +81,14 @@
|
|||
"alert.rate_limited.title": "Feur bevennet",
|
||||
"alert.unexpected.message": "Ur fazi dic'hortozet zo degouezhet.",
|
||||
"alert.unexpected.title": "Hopala !",
|
||||
"alt_text_modal.cancel": "Nullañ",
|
||||
"alt_text_modal.change_thumbnail": "Kemmañ ar velvenn",
|
||||
"alt_text_modal.done": "Graet",
|
||||
"announcement.announcement": "Kemennad",
|
||||
"annual_report.summary.followers.followers": "heulier",
|
||||
"annual_report.summary.highlighted_post.possessive": "{name}",
|
||||
"annual_report.summary.most_used_hashtag.none": "Hini ebet",
|
||||
"annual_report.summary.new_posts.new_posts": "toudoù nevez",
|
||||
"attachments_list.unprocessed": "(ket meret)",
|
||||
"audio.hide": "Kuzhat ar c'hleved",
|
||||
"block_modal.show_less": "Diskouez nebeutoc'h",
|
||||
|
@ -109,9 +114,11 @@
|
|||
"column.blocks": "Implijer·ezed·ien berzet",
|
||||
"column.bookmarks": "Sinedoù",
|
||||
"column.community": "Red-amzer lec'hel",
|
||||
"column.create_list": "Krouiñ ul listenn",
|
||||
"column.direct": "Menegoù prevez",
|
||||
"column.directory": "Mont a-dreuz ar profiloù",
|
||||
"column.domain_blocks": "Domani berzet",
|
||||
"column.edit_list": "Kemmañ al listenn",
|
||||
"column.favourites": "Muiañ-karet",
|
||||
"column.firehose": "Redoù war-eeun",
|
||||
"column.follow_requests": "Rekedoù heuliañ",
|
||||
|
@ -162,9 +169,12 @@
|
|||
"confirmations.delete.message": "Ha sur oc'h e fell deoc'h dilemel an toud-mañ ?",
|
||||
"confirmations.delete_list.confirm": "Dilemel",
|
||||
"confirmations.delete_list.message": "Ha sur eo hoc'h eus c'hoant da zilemel ar roll-mañ da vat ?",
|
||||
"confirmations.delete_list.title": "Dilemel al listenn?",
|
||||
"confirmations.discard_edit_media.confirm": "Nac'hañ",
|
||||
"confirmations.discard_edit_media.message": "Bez ez eus kemmoù n'int ket enrollet e deskrivadur ar media pe ar rakwel, nullañ anezho evelato?",
|
||||
"confirmations.edit.confirm": "Kemmañ",
|
||||
"confirmations.edit.message": "Kemmañ bremañ a zilamo ar gemennadenn emaoc'h o skrivañ. Sur e oc'h e fell deoc'h kenderc'hel ganti?",
|
||||
"confirmations.follow_to_list.title": "Heuliañ an implijer·ez?",
|
||||
"confirmations.logout.confirm": "Digevreañ",
|
||||
"confirmations.logout.message": "Ha sur oc'h e fell deoc'h digevreañ ?",
|
||||
"confirmations.mute.confirm": "Kuzhat",
|
||||
|
@ -266,8 +276,10 @@
|
|||
"footer.privacy_policy": "Reolennoù prevezded",
|
||||
"footer.source_code": "Gwelet ar c'hod mammenn",
|
||||
"footer.status": "Statud",
|
||||
"footer.terms_of_service": "Divizoù implijout hollek",
|
||||
"generic.saved": "Enrollet",
|
||||
"getting_started.heading": "Loc'hañ",
|
||||
"hashtag.admin_moderation": "Digeriñ an etrefas evezhiañ evit #{name}",
|
||||
"hashtag.column_header.tag_mode.all": "ha(g) {additional}",
|
||||
"hashtag.column_header.tag_mode.any": "pe {additional}",
|
||||
"hashtag.column_header.tag_mode.none": "hep {additional}",
|
||||
|
@ -337,8 +349,14 @@
|
|||
"limited_account_hint.action": "Diskouez an aelad memes tra",
|
||||
"limited_account_hint.title": "Kuzhet eo bet ar profil-mañ gant an evezhierien eus {domain}.",
|
||||
"link_preview.author": "Gant {name}",
|
||||
"lists.add_member": "Ouzhpennañ",
|
||||
"lists.add_to_list": "Ouzhpennañ d'al listenn",
|
||||
"lists.create": "Krouiñ",
|
||||
"lists.create_list": "Krouiñ ul listenn",
|
||||
"lists.delete": "Dilemel al listenn",
|
||||
"lists.done": "Graet",
|
||||
"lists.edit": "Kemmañ al listenn",
|
||||
"lists.list_name": "Anv al listenn",
|
||||
"lists.replies_policy.followed": "Pep implijer.ez heuliet",
|
||||
"lists.replies_policy.list": "Izili ar roll",
|
||||
"lists.replies_policy.none": "Den ebet",
|
||||
|
@ -373,11 +391,17 @@
|
|||
"notification.follow": "heuliañ a ra {name} ac'hanoc'h",
|
||||
"notification.follow.name_and_others": "{name} <a>{count, plural, one {hag # den all} two {ha # zen all} few {ha # den all} many {ha # den all} other {ha # den all}}</a> zo o heuliañ ac'hanoc'h",
|
||||
"notification.follow_request": "Gant {name} eo bet goulennet ho heuliañ",
|
||||
"notification.label.reply": "Respont",
|
||||
"notification.moderation-warning.learn_more": "Gouzout hiroc'h",
|
||||
"notification.own_poll": "Echu eo ho sontadeg",
|
||||
"notification.reblog": "Gant {name} eo bet skignet ho toud",
|
||||
"notification.relationships_severance_event.learn_more": "Gouzout hiroc'h",
|
||||
"notification.status": "Emañ {name} o paouez toudañ",
|
||||
"notification.update": "Gant {name} ez eus bet kemmet un toud",
|
||||
"notification_requests.accept": "Asantiñ",
|
||||
"notification_requests.dismiss": "Diverkañ",
|
||||
"notification_requests.edit_selection": "Kemmañ",
|
||||
"notification_requests.exit_selection": "Graet",
|
||||
"notifications.clear": "Skarzhañ ar c'hemennoù",
|
||||
"notifications.clear_confirmation": "Ha sur oc'h e fell deoc'h skarzhañ ho holl kemennoù ?",
|
||||
"notifications.column_settings.admin.report": "Disklêriadurioù nevez :",
|
||||
|
@ -410,6 +434,10 @@
|
|||
"notifications.permission_denied": "Kemennoù war ar burev n'int ket hegerz rak pedadenn aotren ar merdeer a zo bet nullet araok",
|
||||
"notifications.permission_denied_alert": "Kemennoù wa ar burev na c'hellont ket bezañ lezelet, rak aotre ar merdeer a zo bet nac'het a-raok",
|
||||
"notifications.permission_required": "Kemennoù war ar burev n'int ket hegerz abalamour d'an aotre rekis n'eo ket bet roet.",
|
||||
"notifications.policy.accept": "Asantiñ",
|
||||
"notifications.policy.accept_hint": "Diskouez er c’hemennoù",
|
||||
"notifications.policy.drop": "Tremen e-bioù",
|
||||
"notifications.policy.filter": "Silañ",
|
||||
"notifications.policy.filter_new_accounts_title": "Kontoù nevez",
|
||||
"notifications_permission_banner.enable": "Lezel kemennoù war ar burev",
|
||||
"notifications_permission_banner.how_to_control": "Evit reseviñ kemennoù pa ne vez ket digoret Mastodon, lezelit kemennoù war ar burev. Gallout a rit kontrollañ peseurt eskemmoù a c'henel kemennoù war ar burev gant ar {icon} nozelenn a-us kentre ma'z int lezelet.",
|
||||
|
@ -515,6 +543,7 @@
|
|||
"search_results.accounts": "Profiloù",
|
||||
"search_results.all": "Pep tra",
|
||||
"search_results.hashtags": "Hashtagoù",
|
||||
"search_results.no_results": "Disoc'h ebet.",
|
||||
"search_results.see_all": "Gwelet pep tra",
|
||||
"search_results.statuses": "Toudoù",
|
||||
"server_banner.active_users": "implijerien·ezed oberiant",
|
||||
|
@ -579,6 +608,7 @@
|
|||
"subscribed_languages.target": "Cheñch ar yezhoù koumanantet evit {target}",
|
||||
"tabs_bar.home": "Degemer",
|
||||
"tabs_bar.notifications": "Kemennoù",
|
||||
"terms_of_service.title": "Divizoù implijout",
|
||||
"time_remaining.days": "{number, plural,one {# devezh} other {# a zevezh}} a chom",
|
||||
"time_remaining.hours": "{number, plural, one {# eurvezh} other{# eurvezh}} a chom",
|
||||
"time_remaining.minutes": "{number, plural, one {# munut} other{# a vunut}} a chom",
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "nejpoužívanější hashtag",
|
||||
"annual_report.summary.most_used_hashtag.none": "Žádné",
|
||||
"annual_report.summary.new_posts.new_posts": "nové příspěvky",
|
||||
"annual_report.summary.percentile.text": "<topLabel>To vás umisťuje do vrcholu</topLabel><percentage></percentage><bottomLabel>{domain} uživatelů.</bottomLabel>",
|
||||
"annual_report.summary.percentile.text": "<topLabel>To vás umisťuje do horních</topLabel><percentage></percentage><bottomLabel> uživatelů domény {domain}.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "To, že jste zdejší smetánka, zůstane mezi námi ;).",
|
||||
"annual_report.summary.thanks": "Děkujeme, že jste součástí Mastodonu!",
|
||||
"attachments_list.unprocessed": "(nezpracováno)",
|
||||
|
@ -513,9 +513,9 @@
|
|||
"loading_indicator.label": "Načítání…",
|
||||
"media_gallery.hide": "Skrýt",
|
||||
"moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.",
|
||||
"mute_modal.hide_from_notifications": "Skrýt z notifikací",
|
||||
"mute_modal.hide_from_notifications": "Skrýt z oznámení",
|
||||
"mute_modal.hide_options": "Skrýt možnosti",
|
||||
"mute_modal.indefinite": "Dokud je neodkryju",
|
||||
"mute_modal.indefinite": "Dokud je neodeberu ze ztišených",
|
||||
"mute_modal.show_options": "Zobrazit možnosti",
|
||||
"mute_modal.they_can_mention_and_follow": "Mohou vás zmínit a sledovat, ale neuvidíte je.",
|
||||
"mute_modal.they_wont_know": "Nebudou vědět, že byli skryti.",
|
||||
|
@ -524,7 +524,7 @@
|
|||
"mute_modal.you_wont_see_posts": "Stále budou moci vidět vaše příspěvky, ale vy jejich neuvidíte.",
|
||||
"navigation_bar.about": "O aplikaci",
|
||||
"navigation_bar.administration": "Administrace",
|
||||
"navigation_bar.advanced_interface": "Otevřít pokročilé webové rozhraní",
|
||||
"navigation_bar.advanced_interface": "Otevřít v pokročilém webovém rozhraní",
|
||||
"navigation_bar.blocks": "Blokovaní uživatelé",
|
||||
"navigation_bar.bookmarks": "Záložky",
|
||||
"navigation_bar.community_timeline": "Místní časová osa",
|
||||
|
@ -553,13 +553,13 @@
|
|||
"notification.admin.report": "Uživatel {name} nahlásil {target}",
|
||||
"notification.admin.report_account": "{name} nahlásil {count, plural, one {jeden příspěvek} few {# příspěvky} many {# příspěvků} other {# příspěvků}} od {target} za {category}",
|
||||
"notification.admin.report_account_other": "{name} nahlásil {count, plural, one {jeden příspěvek} few {# příspěvky} many {# příspěvků} other {# příspěvků}} od {target}",
|
||||
"notification.admin.report_statuses": "{name} nahlásil {target} za {category}",
|
||||
"notification.admin.report_statuses_other": "{name} nahlásil {target}",
|
||||
"notification.admin.report_statuses": "{name} nahlásili {target} za {category}",
|
||||
"notification.admin.report_statuses_other": "{name} nahlásili {target}",
|
||||
"notification.admin.sign_up": "Uživatel {name} se zaregistroval",
|
||||
"notification.admin.sign_up.name_and_others": "{name} a {count, plural, one {# další} few {# další} many {# dalších} other {# dalších}} se zaregistrovali",
|
||||
"notification.annual_report.message": "Váš #Wrapstodon {year} na Vás čeká! Podívejte se, jak vypadal tento Váš rok na Mastodonu!",
|
||||
"notification.annual_report.view": "Zobrazit #Wrapstodon",
|
||||
"notification.favourite": "Uživatel {name} si oblíbil váš příspěvek",
|
||||
"notification.favourite": "{name} si oblíbil*a váš příspěvek",
|
||||
"notification.favourite.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> si oblíbil} few {<a># další</a> si oblíbili} other {<a># dalších</a> si oblíbilo}} Váš příspěvek",
|
||||
"notification.favourite_pm": "{name} si oblíbil vaši soukromou zmínku",
|
||||
"notification.favourite_pm.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> si oblíbil} few {<a># další</a> si oblíbili} other {<a># dalších</a> si oblíbilo}} Vaši soukromou zmínku",
|
||||
|
@ -578,7 +578,7 @@
|
|||
"notification.moderation_warning.action_delete_statuses": "Některé z vašich příspěvků byly odstraněny.",
|
||||
"notification.moderation_warning.action_disable": "Váš účet je zablokován.",
|
||||
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Některé z vašich příspěvků byly označeny jako citlivé.",
|
||||
"notification.moderation_warning.action_none": "Váš účet obdržel moderační varování.",
|
||||
"notification.moderation_warning.action_none": "Váš účet obdržel varování od moderátorů.",
|
||||
"notification.moderation_warning.action_sensitive": "Vaše příspěvky budou od nynějška označeny jako citlivé.",
|
||||
"notification.moderation_warning.action_silence": "Váš účet byl omezen.",
|
||||
"notification.moderation_warning.action_suspend": "Váš účet byl pozastaven.",
|
||||
|
@ -610,7 +610,7 @@
|
|||
"notification_requests.maximize": "Maximalizovat",
|
||||
"notification_requests.minimize_banner": "Minimalizovat banner filtrovaných oznámení",
|
||||
"notification_requests.notifications_from": "Oznámení od {name}",
|
||||
"notification_requests.title": "Vyfiltrovaná oznámení",
|
||||
"notification_requests.title": "Filtrovaná oznámení",
|
||||
"notification_requests.view": "Zobrazit oznámení",
|
||||
"notifications.clear": "Vyčistit oznámení",
|
||||
"notifications.clear_confirmation": "Opravdu chcete trvale smazat všechna vaše oznámení?",
|
||||
|
@ -773,7 +773,7 @@
|
|||
"report_notification.categories.spam": "Spam",
|
||||
"report_notification.categories.spam_sentence": "spam",
|
||||
"report_notification.categories.violation": "Porušení pravidla",
|
||||
"report_notification.categories.violation_sentence": "porušení pravidla",
|
||||
"report_notification.categories.violation_sentence": "porušení pravidel",
|
||||
"report_notification.open": "Otevřít hlášení",
|
||||
"search.no_recent_searches": "Žádná nedávná vyhledávání",
|
||||
"search.placeholder": "Hledat",
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"account.badges.bot": "Awtomataidd",
|
||||
"account.badges.group": "Grŵp",
|
||||
"account.block": "Blocio @{name}",
|
||||
"account.block_domain": "Blocio parth {domain}",
|
||||
"account.block_domain": "Blocio'r parth {domain}",
|
||||
"account.block_short": "Blocio",
|
||||
"account.blocked": "Blociwyd",
|
||||
"account.cancel_follow_request": "Tynnu cais i ddilyn",
|
||||
|
@ -41,7 +41,7 @@
|
|||
"account.go_to_profile": "Mynd i'r proffil",
|
||||
"account.hide_reblogs": "Cuddio hybiau gan @{name}",
|
||||
"account.in_memoriam": "Er Cof.",
|
||||
"account.joined_short": "Wedi Ymuno",
|
||||
"account.joined_short": "Ymunodd",
|
||||
"account.languages": "Newid ieithoedd wedi tanysgrifio iddyn nhw",
|
||||
"account.link_verified_on": "Gwiriwyd perchnogaeth y ddolen yma ar {date}",
|
||||
"account.locked_info": "Mae'r statws preifatrwydd cyfrif hwn wedi'i osod i fod ar glo. Mae'r perchennog yn adolygu'r sawl sy'n gallu eu dilyn.",
|
||||
|
@ -56,7 +56,7 @@
|
|||
"account.no_bio": "Dim disgrifiad wedi'i gynnig.",
|
||||
"account.open_original_page": "Agor y dudalen wreiddiol",
|
||||
"account.posts": "Postiadau",
|
||||
"account.posts_with_replies": "Postiadau ac atebion",
|
||||
"account.posts_with_replies": "Postiadau ac ymatebion",
|
||||
"account.report": "Adrodd @{name}",
|
||||
"account.requested": "Aros am gymeradwyaeth. Cliciwch er mwyn canslo cais dilyn",
|
||||
"account.requested_follow": "Mae {name} wedi gwneud cais i'ch dilyn",
|
||||
|
@ -85,7 +85,7 @@
|
|||
"alert.rate_limited.title": "Cyfradd gyfyngedig",
|
||||
"alert.unexpected.message": "Digwyddodd gwall annisgwyl.",
|
||||
"alert.unexpected.title": "Wps!",
|
||||
"alt_text_badge.title": "Testun Amgen",
|
||||
"alt_text_badge.title": "Testun amgen",
|
||||
"alt_text_modal.add_alt_text": "Ychwanegu testun amgen",
|
||||
"alt_text_modal.add_text_from_image": "Ychwanegu testun o'r ddelwedd",
|
||||
"alt_text_modal.cancel": "Diddymu",
|
||||
|
@ -110,7 +110,7 @@
|
|||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "hashnod a ddefnyddiwyd fwyaf",
|
||||
"annual_report.summary.most_used_hashtag.none": "Dim",
|
||||
"annual_report.summary.new_posts.new_posts": "postiadau newydd",
|
||||
"annual_report.summary.percentile.text": "<topLabel>Mae hynny'n eich rhoi chi ar y brig</topLabel><percentage></percentage><bottomLabel> o ddefnyddiwr {domain}.</bottomLabel>",
|
||||
"annual_report.summary.percentile.text": "<topLabel>Mae hynny'n eich rhoi chi ymysg y</topLabel><percentage></percentage><bottomLabel>uchaf o ddefnyddwyr {domain}.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "Ni fyddwn yn dweud wrth Bernie.",
|
||||
"annual_report.summary.thanks": "Diolch am fod yn rhan o Mastodon!",
|
||||
"attachments_list.unprocessed": "(heb eu prosesu)",
|
||||
|
@ -142,7 +142,7 @@
|
|||
"closed_registrations_modal.description": "Ar hyn o bryd nid yw'n bosib creu cyfrif ar {domain}, ond cadwch mewn cof nad oes raid i chi gael cyfrif yn benodol ar {domain} i ddefnyddio Mastodon.",
|
||||
"closed_registrations_modal.find_another_server": "Dod o hyd i weinydd arall",
|
||||
"closed_registrations_modal.preamble": "Mae Mastodon wedi'i ddatganoli, felly does dim gwahaniaeth ble rydych chi'n creu eich cyfrif, byddwch chi'n gallu dilyn a rhyngweithio ag unrhyw un ar y gweinydd hwn. Gallwch hyd yn oed ei gynnal un eich hun!",
|
||||
"closed_registrations_modal.title": "Ymgofrestru ar Mastodon",
|
||||
"closed_registrations_modal.title": "Cofrestru ar Mastodon",
|
||||
"column.about": "Ynghylch",
|
||||
"column.blocks": "Defnyddwyr a flociwyd",
|
||||
"column.bookmarks": "Llyfrnodau",
|
||||
|
@ -192,9 +192,9 @@
|
|||
"compose_form.poll.switch_to_multiple": "Newid pleidlais i adael mwy nag un dewis",
|
||||
"compose_form.poll.switch_to_single": "Newid pleidlais i gyfyngu i un dewis",
|
||||
"compose_form.poll.type": "Arddull",
|
||||
"compose_form.publish": "Postiad",
|
||||
"compose_form.publish": "Postio",
|
||||
"compose_form.publish_form": "Postiad newydd",
|
||||
"compose_form.reply": "Ateb",
|
||||
"compose_form.reply": "Ymateb",
|
||||
"compose_form.save_changes": "Diweddaru",
|
||||
"compose_form.spoiler.marked": "Dileu rhybudd cynnwys",
|
||||
"compose_form.spoiler.unmarked": "Ychwanegu rhybudd cynnwys",
|
||||
|
@ -226,7 +226,7 @@
|
|||
"confirmations.redraft.confirm": "Dileu ac ailddrafftio",
|
||||
"confirmations.redraft.message": "Ydych chi wir eisiau'r dileu'r postiad hwn a'i ailddrafftio? Bydd ffefrynnau a hybiau'n cael eu colli, a bydd atebion i'r post gwreiddiol yn mynd yn amddifad.",
|
||||
"confirmations.redraft.title": "Dileu & ailddraftio postiad?",
|
||||
"confirmations.reply.confirm": "Ateb",
|
||||
"confirmations.reply.confirm": "Ymateb",
|
||||
"confirmations.reply.message": "Bydd ateb nawr yn cymryd lle y neges yr ydych yn cyfansoddi ar hyn o bryd. Ydych chi'n siŵr eich bod am barhau?",
|
||||
"confirmations.reply.title": "Trosysgrifo'r postiad?",
|
||||
"confirmations.unfollow.confirm": "Dad-ddilyn",
|
||||
|
@ -248,8 +248,8 @@
|
|||
"directory.recently_active": "Ar-lein yn ddiweddar",
|
||||
"disabled_account_banner.account_settings": "Gosodiadau'r cyfrif",
|
||||
"disabled_account_banner.text": "Mae eich cyfrif {disabledAccount} wedi ei analluogi ar hyn o bryd.",
|
||||
"dismissable_banner.community_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl sydd â chyfrifon ar {domain}.",
|
||||
"dismissable_banner.dismiss": "Cau",
|
||||
"dismissable_banner.community_timeline": "Dyma'r postiadau cyhoeddus diweddaraf gan bobl y caiff eu cyfrifon eu cynnal ar {domain}.",
|
||||
"dismissable_banner.dismiss": "Diystyru",
|
||||
"dismissable_banner.explore_links": "Y straeon newyddion hyn yw'r rhai sy'n cael eu rhannu fwyaf ar y ffederasiwn heddiw. Mae straeon newyddion mwy diweddar sy'n cael eu postio gan fwy o amrywiaeth o bobl yn cael eu graddio'n uwch.",
|
||||
"dismissable_banner.explore_statuses": "Mae'r postiadau hyn o bob rhan o'r ffedysawd yn cael mwy o sylw heddiw. Mae postiadau mwy diweddar sydd â mwy o hybu a ffefrynnu'n cael eu graddio'n uwch.",
|
||||
"dismissable_banner.explore_tags": "Mae'r hashnodau hyn ar gynnydd y ffedysawd heddiw. Mae hashnodau sy'n cael eu defnyddio gan fwy o bobl amrywiol yn cael eu graddio'n uwch.",
|
||||
|
@ -398,10 +398,10 @@
|
|||
"hints.profiles.see_more_followers": "Gweld mwy o ddilynwyr ar {domain}",
|
||||
"hints.profiles.see_more_follows": "Gweld mwy o 'yn dilyn' ar {domain}",
|
||||
"hints.profiles.see_more_posts": "Gweld mwy o bostiadau ar {domain}",
|
||||
"hints.threads.replies_may_be_missing": "Mae'n bosibl y bydd atebion gan weinyddion eraill ar goll.",
|
||||
"hints.threads.see_more": "Gweld mwy o atebion ar {domain}",
|
||||
"hints.threads.replies_may_be_missing": "Mae'n bosibl y bydd ymatebion gan weinyddion eraill ar goll.",
|
||||
"hints.threads.see_more": "Gweld mwy o ymatebion ar {domain}",
|
||||
"home.column_settings.show_reblogs": "Dangos hybiau",
|
||||
"home.column_settings.show_replies": "Dangos atebion",
|
||||
"home.column_settings.show_replies": "Dangos ymatebion",
|
||||
"home.hide_announcements": "Cuddio cyhoeddiadau",
|
||||
"home.pending_critical_update.body": "Diweddarwch eich gweinydd Mastodon cyn gynted â phosibl!",
|
||||
"home.pending_critical_update.link": "Gweld diweddariadau",
|
||||
|
@ -423,7 +423,7 @@
|
|||
"interaction_modal.action.favourite": "I barhau, mae angen i chi hoffi o'ch cyfrif.",
|
||||
"interaction_modal.action.follow": "I barhau, mae angen i chi ddilyn o'ch cyfrif.",
|
||||
"interaction_modal.action.reblog": "I barhau, mae angen i chi ail-flogio o'ch cyfrif.",
|
||||
"interaction_modal.action.reply": "I barhau, mae angen i chi ateb o'ch cyfrif.",
|
||||
"interaction_modal.action.reply": "I barhau, mae angen i chi ymateb o'ch cyfrif.",
|
||||
"interaction_modal.action.vote": "I barhau, mae angen i chi bleidleisio o'ch cyfrif.",
|
||||
"interaction_modal.go": "Mynd",
|
||||
"interaction_modal.no_account_yet": "Dim cyfrif eto?",
|
||||
|
@ -462,7 +462,7 @@
|
|||
"keyboard_shortcuts.open_media": "Agor cyfryngau",
|
||||
"keyboard_shortcuts.pinned": "Agor rhestr postiadau wedi'u pinio",
|
||||
"keyboard_shortcuts.profile": "Agor proffil yr awdur",
|
||||
"keyboard_shortcuts.reply": "Ateb i bostiad",
|
||||
"keyboard_shortcuts.reply": "Ymateb i bostiad",
|
||||
"keyboard_shortcuts.requests": "Agor rhestr ceisiadau dilyn",
|
||||
"keyboard_shortcuts.search": "Ffocysu ar y bar chwilio",
|
||||
"keyboard_shortcuts.spoilers": "Dangos/cuddio'r maes CW",
|
||||
|
@ -489,9 +489,9 @@
|
|||
"lists.create": "Creu",
|
||||
"lists.create_a_list_to_organize": "Creu rhestr newydd i drefnu eich llif Cartref",
|
||||
"lists.create_list": "Creu rhestr",
|
||||
"lists.delete": "Dileu rhestr",
|
||||
"lists.delete": "Dileu'r rhestr",
|
||||
"lists.done": "Wedi gorffen",
|
||||
"lists.edit": "Golygu rhestr",
|
||||
"lists.edit": "Golygu'r rhestr",
|
||||
"lists.exclusive": "Cuddio aelodau yn y Cartref",
|
||||
"lists.exclusive_hint": "Os oes rhywun ar y rhestr hon, cuddiwch nhw yn eich llif Cartref i osgoi gweld eu postiadau ddwywaith.",
|
||||
"lists.find_users_to_add": "Canfod defnyddwyr i'w hychwanegu",
|
||||
|
@ -508,11 +508,11 @@
|
|||
"lists.replies_policy.none": "Neb",
|
||||
"lists.save": "Cadw",
|
||||
"lists.search": "Chwilio",
|
||||
"lists.show_replies_to": "Cynhwyswch atebion gan aelodau'r rhestr i",
|
||||
"lists.show_replies_to": "Cynnwys ymatebion gan aelodau'r rhestr i",
|
||||
"load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}",
|
||||
"loading_indicator.label": "Yn llwytho…",
|
||||
"media_gallery.hide": "Cuddio",
|
||||
"moved_to_account_banner.text": "Ar hyn y bryd, mae eich cyfrif {disabledAccount} wedi ei analluogi am i chi symud i {movedToAccount}.",
|
||||
"moved_to_account_banner.text": "Mae eich cyfrif {disabledAccount} wedi ei analluogi ar hyn o bryd am i chi symud i {movedToAccount}.",
|
||||
"mute_modal.hide_from_notifications": "Cuddio rhag hysbysiadau",
|
||||
"mute_modal.hide_options": "Cuddio'r dewis",
|
||||
"mute_modal.indefinite": "Nes i mi eu dad-dewi",
|
||||
|
@ -550,7 +550,7 @@
|
|||
"navigation_bar.search": "Chwilio",
|
||||
"navigation_bar.security": "Diogelwch",
|
||||
"not_signed_in_indicator.not_signed_in": "Rhaid i chi fewngofnodi i weld yr adnodd hwn.",
|
||||
"notification.admin.report": "Adroddwyd ar {name} {target}",
|
||||
"notification.admin.report": "Adroddodd {name} {target}",
|
||||
"notification.admin.report_account": "Adroddodd {name} {count, plural, one {un postiad} other {# postiad}} gan {target} oherwydd {category}",
|
||||
"notification.admin.report_account_other": "Adroddodd {name} {count, plural, one {un postiad} two {# bostiad} few {# postiad} other {# postiad}} gan {target}",
|
||||
"notification.admin.report_statuses": "Adroddodd {name} {target} ar gyfer {category}",
|
||||
|
@ -569,8 +569,8 @@
|
|||
"notification.follow_request.name_and_others": "Mae {name} a{count, plural, one {# arall} other {# arall}} wedi gofyn i'ch dilyn chi",
|
||||
"notification.label.mention": "Crybwyll",
|
||||
"notification.label.private_mention": "Crybwyll preifat",
|
||||
"notification.label.private_reply": "Ateb preifat",
|
||||
"notification.label.reply": "Ateb",
|
||||
"notification.label.private_reply": "Ymateb preifat",
|
||||
"notification.label.reply": "Ymateb",
|
||||
"notification.mention": "Crybwyll",
|
||||
"notification.mentioned_you": "Rydych wedi'ch crybwyll gan {name}",
|
||||
"notification.moderation-warning.learn_more": "Dysgu mwy",
|
||||
|
@ -731,7 +731,7 @@
|
|||
"report.categories.other": "Arall",
|
||||
"report.categories.spam": "Sbam",
|
||||
"report.categories.violation": "Mae cynnwys yn torri un neu fwy o reolau'r gweinydd",
|
||||
"report.category.subtitle": "Dewiswch y gyfatebiaeth gorau",
|
||||
"report.category.subtitle": "Dewiswch yr ateb gorau",
|
||||
"report.category.title": "Beth sy'n digwydd gyda'r {type} yma?",
|
||||
"report.category.title_account": "proffil",
|
||||
"report.category.title_status": "post",
|
||||
|
@ -853,7 +853,7 @@
|
|||
"status.remove_favourite": "Tynnu o'r ffefrynnau",
|
||||
"status.replied_in_thread": "Atebodd mewn edefyn",
|
||||
"status.replied_to": "Wedi ateb {name}",
|
||||
"status.reply": "Ateb",
|
||||
"status.reply": "Ymateb",
|
||||
"status.replyAll": "Ateb i edefyn",
|
||||
"status.report": "Adrodd ar @{name}",
|
||||
"status.sensitive_warning": "Cynnwys sensitif",
|
||||
|
|
|
@ -562,6 +562,7 @@
|
|||
"notification.favourite": "{name} märkis su postituse lemmikuks",
|
||||
"notification.favourite.name_and_others_with_link": "{name} ja <a>{count, plural, one {# veel} other {# teist}}</a> märkis su postituse lemmikuks",
|
||||
"notification.favourite_pm": "{name} märkis sinu privaatse mainimise lemmikuks",
|
||||
"notification.favourite_pm.name_and_others_with_link": "{name} ja <a>{count, plural, one {# veel} other {# veel}}</a> märkisid su privaatse mainimise lemmikuks",
|
||||
"notification.follow": "{name} alustas su jälgimist",
|
||||
"notification.follow.name_and_others": "{name} ja veel {count, plural, one {# kasutaja} other {# kasutajat}} hakkas sind jälgima",
|
||||
"notification.follow_request": "{name} soovib sind jälgida",
|
||||
|
@ -696,6 +697,7 @@
|
|||
"poll_button.remove_poll": "Eemalda küsitlus",
|
||||
"privacy.change": "Muuda postituse nähtavust",
|
||||
"privacy.direct.long": "Kõik postituses mainitud",
|
||||
"privacy.direct.short": "Privaatne mainimine",
|
||||
"privacy.private.long": "Ainult jälgijad",
|
||||
"privacy.private.short": "Jälgijad",
|
||||
"privacy.public.long": "Nii kasutajad kui mittekasutajad",
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"account.featured_tags.last_status_never": "Aucune publication",
|
||||
"account.featured_tags.title": "Hashtags inclus de {name}",
|
||||
"account.follow": "Suivre",
|
||||
"account.follow_back": "S'abonner en retour",
|
||||
"account.follow_back": "Suivre en retour",
|
||||
"account.followers": "abonné·e·s",
|
||||
"account.followers.empty": "Personne ne suit ce compte pour l'instant.",
|
||||
"account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}",
|
||||
|
@ -119,7 +119,7 @@
|
|||
"block_modal.show_less": "Afficher moins",
|
||||
"block_modal.show_more": "Afficher plus",
|
||||
"block_modal.they_cant_mention": "Il ne peut pas vous mentionner ou vous suivre.",
|
||||
"block_modal.they_cant_see_posts": "Il peut toujours voir vos messages, mais vous ne verrez pas les siens.",
|
||||
"block_modal.they_cant_see_posts": "Il ne peut plus voir vos messages et vous ne verrez plus les siens.",
|
||||
"block_modal.they_will_know": "Il peut voir qu'il est bloqué.",
|
||||
"block_modal.title": "Bloquer le compte ?",
|
||||
"block_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.",
|
||||
|
@ -872,7 +872,9 @@
|
|||
"subscribed_languages.target": "Changer les langues abonnées pour {target}",
|
||||
"tabs_bar.home": "Accueil",
|
||||
"tabs_bar.notifications": "Notifications",
|
||||
"terms_of_service.effective_as_of": "En vigueur à compter du {date}",
|
||||
"terms_of_service.title": "Conditions d'utilisation",
|
||||
"terms_of_service.upcoming_changes_on": "Modifications à venir le {date}",
|
||||
"time_remaining.days": "{number, plural, one {# jour restant} other {# jours restants}}",
|
||||
"time_remaining.hours": "{number, plural, one {# heure restante} other {# heures restantes}}",
|
||||
"time_remaining.minutes": "{number, plural, one {# minute restante} other {# minutes restantes}}",
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"about.contact": "Contact :",
|
||||
"about.disclaimer": "Mastodon est un logiciel libre, open-source et une marque déposée de Mastodon gGmbH.",
|
||||
"about.domain_blocks.no_reason_available": "Raison non disponible",
|
||||
"about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec les utilisateur⋅rice⋅s de n'importe quel autre serveur dans le fédiverse. Voici les exceptions qui ont été faites sur ce serveur en particulier.",
|
||||
"about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec les utilisateur⋅rices de n'importe quel autre serveur dans le fédivers. Voici les exceptions qui ont été faites sur ce serveur-là.",
|
||||
"about.domain_blocks.silenced.explanation": "Vous ne verrez généralement pas les profils et le contenu de ce serveur, à moins que vous ne les recherchiez explicitement ou que vous ne choisissiez de les suivre.",
|
||||
"about.domain_blocks.silenced.title": "Limité",
|
||||
"about.domain_blocks.suspended.explanation": "Aucune donnée de ce serveur ne sera traitée, enregistrée ou échangée, rendant impossible toute interaction ou communication avec les comptes de ce serveur.",
|
||||
|
@ -19,7 +19,7 @@
|
|||
"account.block_domain": "Bloquer le domaine {domain}",
|
||||
"account.block_short": "Bloquer",
|
||||
"account.blocked": "Bloqué·e",
|
||||
"account.cancel_follow_request": "Annuler le suivi",
|
||||
"account.cancel_follow_request": "Annuler l'abonnement",
|
||||
"account.copy": "Copier le lien vers le profil",
|
||||
"account.direct": "Mention privée @{name}",
|
||||
"account.disable_notifications": "Ne plus me notifier quand @{name} publie quelque chose",
|
||||
|
@ -31,18 +31,18 @@
|
|||
"account.featured_tags.last_status_never": "Aucun message",
|
||||
"account.featured_tags.title": "Les hashtags en vedette de {name}",
|
||||
"account.follow": "Suivre",
|
||||
"account.follow_back": "S'abonner en retour",
|
||||
"account.follow_back": "Suivre en retour",
|
||||
"account.followers": "Abonné·e·s",
|
||||
"account.followers.empty": "Personne ne suit cet·te utilisateur·rice pour l’instant.",
|
||||
"account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}",
|
||||
"account.following": "Abonnements",
|
||||
"account.following_counter": "{count, plural, one {{counter} abonnement} other {{counter} abonnements}}",
|
||||
"account.follows.empty": "Cet·te utilisateur·rice ne suit personne pour l’instant.",
|
||||
"account.go_to_profile": "Aller au profil",
|
||||
"account.go_to_profile": "Voir le profil",
|
||||
"account.hide_reblogs": "Masquer les partages de @{name}",
|
||||
"account.in_memoriam": "En mémoire de.",
|
||||
"account.joined_short": "Ici depuis",
|
||||
"account.languages": "Changer les langues abonnées",
|
||||
"account.languages": "Modifier les langues d'abonnements",
|
||||
"account.link_verified_on": "La propriété de ce lien a été vérifiée le {date}",
|
||||
"account.locked_info": "Ce compte est privé. Son ou sa propriétaire approuve manuellement qui peut le suivre.",
|
||||
"account.media": "Médias",
|
||||
|
@ -119,7 +119,7 @@
|
|||
"block_modal.show_less": "Afficher moins",
|
||||
"block_modal.show_more": "Afficher plus",
|
||||
"block_modal.they_cant_mention": "Il ne peut pas vous mentionner ou vous suivre.",
|
||||
"block_modal.they_cant_see_posts": "Il peut toujours voir vos messages, mais vous ne verrez pas les siens.",
|
||||
"block_modal.they_cant_see_posts": "Il ne peut plus voir vos messages et vous ne verrez plus les siens.",
|
||||
"block_modal.they_will_know": "Il peut voir qu'il est bloqué.",
|
||||
"block_modal.title": "Bloquer le compte ?",
|
||||
"block_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.",
|
||||
|
@ -872,7 +872,9 @@
|
|||
"subscribed_languages.target": "Changer les langues abonnées pour {target}",
|
||||
"tabs_bar.home": "Accueil",
|
||||
"tabs_bar.notifications": "Notifications",
|
||||
"terms_of_service.effective_as_of": "En vigueur à compter du {date}",
|
||||
"terms_of_service.title": "Conditions d'utilisation",
|
||||
"terms_of_service.upcoming_changes_on": "Modifications à venir le {date}",
|
||||
"time_remaining.days": "{number, plural, one {# jour restant} other {# jours restants}}",
|
||||
"time_remaining.hours": "{number, plural, one {# heure restante} other {# heures restantes}}",
|
||||
"time_remaining.minutes": "{number, plural, one {# minute restante} other {# minutes restantes}}",
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
"account.followers.empty": "Ancora nessuno segue questo utente.",
|
||||
"account.followers_counter": "{count, plural, one {{counter} seguace} other {{counter} seguaci}}",
|
||||
"account.following": "Seguiti",
|
||||
"account.following_counter": "{count, plural, one {{counter} segui} other {{counter} segui}}",
|
||||
"account.following_counter": "{count, plural, one {{counter} segui} other {{counter} seguiti}}",
|
||||
"account.follows.empty": "Questo utente non segue ancora nessuno.",
|
||||
"account.go_to_profile": "Vai al profilo",
|
||||
"account.hide_reblogs": "Nascondi condivisioni da @{name}",
|
||||
|
|
|
@ -6,20 +6,20 @@
|
|||
"account.badges.group": "ჯგუფი",
|
||||
"account.block": "დაბლოკე @{name}",
|
||||
"account.block_domain": "დაიმალოს ყველაფერი დომენიდან {domain}",
|
||||
"account.blocked": "დაიბლოკა",
|
||||
"account.blocked": "დაბლოკილია",
|
||||
"account.cancel_follow_request": "Withdraw follow request",
|
||||
"account.domain_blocked": "დომენი დამალულია",
|
||||
"account.edit_profile": "პროფილის ცვლილება",
|
||||
"account.endorse": "გამორჩევა პროფილზე",
|
||||
"account.featured_tags.last_status_never": "პოსტები არ არის",
|
||||
"account.featured_tags.last_status_never": "პოსტების გარეშე",
|
||||
"account.follow": "გაყოლა",
|
||||
"account.followers": "მიმდევრები",
|
||||
"account.hide_reblogs": "დაიმალოს ბუსტები @{name}-სგან",
|
||||
"account.media": "მედია",
|
||||
"account.mention": "ასახელეთ @{name}",
|
||||
"account.mute": "გააჩუმე @{name}",
|
||||
"account.muted": "გაჩუმებული",
|
||||
"account.posts": "ტუტები",
|
||||
"account.muted": "დადუმებულია",
|
||||
"account.posts": "პოსტები",
|
||||
"account.posts_with_replies": "ტუტები და პასუხები",
|
||||
"account.report": "დაარეპორტე @{name}",
|
||||
"account.requested": "დამტკიცების მოლოდინში. დააწკაპუნეთ რომ უარყოთ დადევნების მოთხონვა",
|
||||
|
@ -42,7 +42,7 @@
|
|||
"column.community": "ლოკალური თაიმლაინი",
|
||||
"column.domain_blocks": "დამალული დომენები",
|
||||
"column.follow_requests": "დადევნების მოთხოვნები",
|
||||
"column.home": "სახლი",
|
||||
"column.home": "საწყისი",
|
||||
"column.lists": "სიები",
|
||||
"column.mutes": "გაჩუმებული მომხმარებლები",
|
||||
"column.notifications": "შეტყობინებები",
|
||||
|
@ -52,9 +52,9 @@
|
|||
"column_header.hide_settings": "პარამეტრების დამალვა",
|
||||
"column_header.moveLeft_settings": "სვეტის მარცხნივ გადატანა",
|
||||
"column_header.moveRight_settings": "სვეტის მარჯვნივ გადატანა",
|
||||
"column_header.pin": "აპინვა",
|
||||
"column_header.pin": "მიმაგრება",
|
||||
"column_header.show_settings": "პარამეტრების ჩვენება",
|
||||
"column_header.unpin": "პინის მოხსნა",
|
||||
"column_header.unpin": "მოხსნა",
|
||||
"column_subheading.settings": "პარამეტრები",
|
||||
"community.column_settings.media_only": "მხოლოდ მედია",
|
||||
"compose_form.direct_message_warning_learn_more": "გაიგე მეტი",
|
||||
|
@ -66,21 +66,21 @@
|
|||
"compose_form.publish_form": "Publish",
|
||||
"compose_form.spoiler.marked": "გაფრთხილების უკან ტექსტი დამალულია",
|
||||
"compose_form.spoiler.unmarked": "ტექსტი არაა დამალული",
|
||||
"confirmation_modal.cancel": "უარყოფა",
|
||||
"confirmation_modal.cancel": "გაუქმება",
|
||||
"confirmations.block.confirm": "ბლოკი",
|
||||
"confirmations.delete.confirm": "გაუქმება",
|
||||
"confirmations.delete.confirm": "წაშლა",
|
||||
"confirmations.delete.message": "დარწმუნებული ხართ, გსურთ გააუქმოთ ეს სტატუსი?",
|
||||
"confirmations.delete_list.confirm": "გაუქმება",
|
||||
"confirmations.delete_list.confirm": "წაშლა",
|
||||
"confirmations.delete_list.message": "დარწმუნებული ხართ, გსურთ სამუდამოდ გააუქმოთ ეს სია?",
|
||||
"confirmations.mute.confirm": "გაჩუმება",
|
||||
"confirmations.mute.confirm": "დადუმება",
|
||||
"confirmations.redraft.confirm": "გაუქმება და გადანაწილება",
|
||||
"confirmations.unfollow.confirm": "ნუღარ მიჰყვები",
|
||||
"confirmations.unfollow.message": "დარწმუნებული ხართ, აღარ გსურთ მიჰყვებოდეთ {name}-ს?",
|
||||
"embed.instructions": "ეს სტატუსი ჩასვით თქვენს ვებ-საიტზე შემდეგი კოდის კოპირებით.",
|
||||
"embed.preview": "ესაა თუ როგორც გამოჩნდება:",
|
||||
"emoji_button.activity": "აქტივობა",
|
||||
"emoji_button.custom": "პერსონალიზირებული",
|
||||
"emoji_button.flags": "დროშები",
|
||||
"emoji_button.custom": "მომხმარებლის",
|
||||
"emoji_button.flags": "ალმები",
|
||||
"emoji_button.food": "საჭმელი და სასლმელი",
|
||||
"emoji_button.label": "ემოჯის ჩასმა",
|
||||
"emoji_button.nature": "ბუმება",
|
||||
|
@ -119,7 +119,7 @@
|
|||
"keyboard_shortcuts.federated": "to open federated timeline",
|
||||
"keyboard_shortcuts.heading": "კლავიატურის სწრაფი ბმულები",
|
||||
"keyboard_shortcuts.home": "to open home timeline",
|
||||
"keyboard_shortcuts.hotkey": "ცხელი კლავიში",
|
||||
"keyboard_shortcuts.hotkey": "მალსახმობი ღილაკი",
|
||||
"keyboard_shortcuts.legend": "ამ ლეგენდის გამოსაჩენად",
|
||||
"keyboard_shortcuts.local": "to open local timeline",
|
||||
"keyboard_shortcuts.mention": "ავტორის დასახელებლად",
|
||||
|
@ -180,20 +180,20 @@
|
|||
"relative_time.just_now": "ახლა",
|
||||
"relative_time.minutes": "{number}წთ",
|
||||
"relative_time.seconds": "{number}წმ",
|
||||
"reply_indicator.cancel": "უარყოფა",
|
||||
"reply_indicator.cancel": "გაუქმება",
|
||||
"report.forward": "ფორვარდი {target}-ს",
|
||||
"report.forward_hint": "ანგარიში სხვა სერვერიდანაა. გავაგზავნოთ რეპორტის ანონიმური ასლიც?",
|
||||
"report.placeholder": "დამატებითი კომენტარები",
|
||||
"report.submit": "დასრულება",
|
||||
"report.submit": "გადაცემა",
|
||||
"report.target": "არეპორტებთ {target}",
|
||||
"report_notification.attached_statuses": "{count, plural, one {# post} other {# posts}} attached",
|
||||
"search.placeholder": "ძებნა",
|
||||
"search_results.hashtags": "ჰეშტეგები",
|
||||
"search_results.statuses": "ტუტები",
|
||||
"sign_in_banner.sign_in": "Sign in",
|
||||
"search_results.statuses": "პოსტები",
|
||||
"sign_in_banner.sign_in": "შესვლა",
|
||||
"status.admin_status": "Open this status in the moderation interface",
|
||||
"status.block": "დაბლოკე @{name}",
|
||||
"status.cancel_reblog_private": "ბუსტის მოშორება",
|
||||
"status.cancel_reblog_private": "ბუსტის მოხსნა",
|
||||
"status.cannot_reblog": "ეს პოსტი ვერ დაიბუსტება",
|
||||
"status.copy": "Copy link to status",
|
||||
"status.delete": "წაშლა",
|
||||
|
@ -222,7 +222,7 @@
|
|||
"status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}",
|
||||
"status.unmute_conversation": "საუბარზე გაჩუმების მოშორება",
|
||||
"status.unpin": "პროფილიდან პინის მოშორება",
|
||||
"tabs_bar.home": "სახლი",
|
||||
"tabs_bar.home": "საწყისი",
|
||||
"tabs_bar.notifications": "შეტყობინებები",
|
||||
"trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}",
|
||||
"ui.beforeunload": "თქვენი დრაფტი გაუქმდება თუ დატოვებთ მასტოდონს.",
|
||||
|
|
|
@ -874,6 +874,7 @@
|
|||
"tabs_bar.notifications": "알림",
|
||||
"terms_of_service.effective_as_of": "{date}부터 적용됨",
|
||||
"terms_of_service.title": "이용 약관",
|
||||
"terms_of_service.upcoming_changes_on": "{date}에 예정된 변경사항",
|
||||
"time_remaining.days": "{number} 일 남음",
|
||||
"time_remaining.hours": "{number} 시간 남음",
|
||||
"time_remaining.minutes": "{number} 분 남음",
|
||||
|
|
|
@ -173,7 +173,7 @@
|
|||
"compose.published.open": "Atvērt",
|
||||
"compose.saved.body": "Ziņa saglabāta.",
|
||||
"compose_form.direct_message_warning_learn_more": "Uzzināt vairāk",
|
||||
"compose_form.encryption_warning": "Mastodon ieraksti nav pilnībā šifrēti. Nedalies ar jebkādu jutīgu informāciju caur Mastodon!",
|
||||
"compose_form.encryption_warning": "Mastodon ieraksti nav pilnībā šifrēti. Nedalies ar jebkādu jūtīgu informāciju caur Mastodon!",
|
||||
"compose_form.hashtag_warning": "Šis ieraksts netiks uzrādīts nevienā tēmturī, jo tas nav redzams visiem. Tikai visiem redzamos ierakstus var meklēt pēc tēmtura.",
|
||||
"compose_form.lock_disclaimer": "Tavs konts nav {locked}. Ikviens var Tev sekot, lai redzētu tikai sekotājiem paredzētos ierakstus.",
|
||||
"compose_form.lock_disclaimer.lock": "slēgts",
|
||||
|
@ -474,9 +474,9 @@
|
|||
"notification.moderation_warning": "Ir saņemts satura pārraudzības brīdinājums",
|
||||
"notification.moderation_warning.action_delete_statuses": "Daži no Taviem ierakstiem tika noņemti.",
|
||||
"notification.moderation_warning.action_disable": "Tavs konts tika atspējots.",
|
||||
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Daži no Taviem ierakstiem tika atzīmēti kā jutīgi.",
|
||||
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Daži no Taviem ierakstiem tika atzīmēti kā jūtīgi.",
|
||||
"notification.moderation_warning.action_none": "Konts ir saņēmis satura pārraudzības brīdinājumu.",
|
||||
"notification.moderation_warning.action_sensitive": "Tavi ieraksti turpmāk tiks atzīmēti kā jutīgi.",
|
||||
"notification.moderation_warning.action_sensitive": "Tavi ieraksti turpmāk tiks atzīmēti kā jūtīgi.",
|
||||
"notification.moderation_warning.action_silence": "Tavs konts tika ierobežots.",
|
||||
"notification.moderation_warning.action_suspend": "Tava konta darbība tika apturēta.",
|
||||
"notification.own_poll": "Tava aptauja ir noslēgusies",
|
||||
|
@ -702,7 +702,7 @@
|
|||
"status.reply": "Atbildēt",
|
||||
"status.replyAll": "Atbildēt uz tematu",
|
||||
"status.report": "Ziņot par @{name}",
|
||||
"status.sensitive_warning": "Sensitīvs saturs",
|
||||
"status.sensitive_warning": "Jūtīgs saturs",
|
||||
"status.share": "Kopīgot",
|
||||
"status.show_less_all": "Rādīt mazāk visiem",
|
||||
"status.show_more_all": "Rādīt vairāk visiem",
|
||||
|
|
|
@ -309,6 +309,15 @@
|
|||
"empty_column.followed_tags": "Lí iáu buē收著任何ê hashtag。Nā是lí收著,ē佇tsia顯示。",
|
||||
"empty_column.hashtag": "Tsit ê hashtag內底無物件。",
|
||||
"empty_column.home": "Lí tshù ê時間線是空ê!跟tuè別lâng來kā充滿。",
|
||||
"empty_column.list": "Tsit張列單內底iáu bô物件。若是列單內底ê成員貼新ê PO文,in ē tī tsia顯示。",
|
||||
"empty_column.mutes": "Lí iáu無消音任何用者。",
|
||||
"empty_column.notification_requests": "清hōo空ah!內底無物件。若是lí收著新ê通知,ē根據lí ê設定,佇tsia出現。",
|
||||
"empty_column.notifications": "Lí iáu無收著任何通知。Nā別lâng kap lí互動,lí ē佇tsia看著。",
|
||||
"empty_column.public": "內底無物件!寫beh公開ê PO文,á是主動跟tuè別ê服侍器ê用者,來加添內容。",
|
||||
"error.unexpected_crash.explanation": "因為原始碼內底有錯誤,á是瀏覽器相容出tshê,tsit頁bē當正確顯示。",
|
||||
"error.unexpected_crash.explanation_addons": "Tsit頁bē當正確顯示,可能是瀏覽器附ê功能,á是自動翻譯工具所致。",
|
||||
"error.unexpected_crash.next_steps": "請試更新tsit頁。若是bē當改善,lí iáu是ē當改使用無kâng ê瀏覽器,á是app,來用Mastodon。",
|
||||
"error.unexpected_crash.next_steps_addons": "請試kā in停止使用,suà落來更新tsit頁。若是bē當改善,lí iáu是ē當改使用無kâng ê瀏覽器,á是app,來用Mastodon。",
|
||||
"errors.unexpected_crash.copy_stacktrace": "Khóo-pih stacktrace kàu剪貼pang-á",
|
||||
"errors.unexpected_crash.report_issue": "報告問題",
|
||||
"explore.suggested_follows": "用者",
|
||||
|
@ -345,6 +354,11 @@
|
|||
"follow_suggestions.dismiss": "Mài koh顯示。",
|
||||
"follow_suggestions.featured_longer": "{domain} 團隊所揀ê",
|
||||
"follow_suggestions.friends_of_friends_longer": "時行佇lí所tuè ê lâng",
|
||||
"follow_suggestions.hints.featured": "Tsit ê個人資料是 {domain} 團隊特別揀ê。",
|
||||
"follow_suggestions.hints.friends_of_friends": "Tsit ê個人資料tī lí跟tuè ê lâng之間真流行。",
|
||||
"follow_suggestions.hints.most_followed": "Tsit ê個人資料是 {domain} 內,有足tsē跟tuè者ê其中tsit ê。",
|
||||
"follow_suggestions.hints.most_interactions": "Tsit ê個人資料tsi̍t-tsām-á佇 {domain} 有得著真tsē關注。",
|
||||
"follow_suggestions.hints.similar_to_recently_followed": "Tsit ê個人資料kap lí最近跟tuè ê口座相siâng。",
|
||||
"follow_suggestions.personalized_suggestion": "個人化ê推薦",
|
||||
"follow_suggestions.popular_suggestion": "流行ê推薦",
|
||||
"follow_suggestions.popular_suggestion_longer": "佇{domain} 足有lâng緣",
|
||||
|
@ -378,6 +392,9 @@
|
|||
"hashtag.follow": "跟tuè hashtag",
|
||||
"hashtag.unfollow": "取消跟tuè hashtag",
|
||||
"hashtags.and_other": "……kap 其他 {count, plural, other {# ê}}",
|
||||
"hints.profiles.followers_may_be_missing": "Tsit ê個人資料ê跟tuè者資訊可能有落勾ê。",
|
||||
"hints.profiles.follows_may_be_missing": "Tsit ê口座所跟tuè ê ê資訊可能有落勾ê。",
|
||||
"hints.profiles.posts_may_be_missing": "Tsit ê口座ê tsi̍t kuá PO文可能有落勾ê。",
|
||||
"hints.profiles.see_more_followers": "佇 {domain} 看koh khah tsē跟tuè lí ê",
|
||||
"hints.profiles.see_more_follows": "佇 {domain} 看koh khah tsē lí跟tuè ê",
|
||||
"hints.profiles.see_more_posts": "佇 {domain} 看koh khah tsē ê PO文",
|
||||
|
@ -390,6 +407,67 @@
|
|||
"home.pending_critical_update.link": "看更新內容",
|
||||
"home.pending_critical_update.title": "有重要ê安全更新!",
|
||||
"home.show_announcements": "顯示公告",
|
||||
"ignore_notifications_modal.disclaimer": "Lí所忽略in ê通知ê用者,Mastodonbē當kā lí通知。忽略通知bē當阻擋訊息ê寄送。",
|
||||
"ignore_notifications_modal.filter_instead": "改做過濾",
|
||||
"ignore_notifications_modal.filter_to_act_users": "Lí猶原ē當接受、拒絕猶是檢舉用者",
|
||||
"ignore_notifications_modal.filter_to_avoid_confusion": "過濾ē當避免可能ê bē分明。",
|
||||
"ignore_notifications_modal.filter_to_review_separately": "Lí ē當個別檢視所過濾ê通知",
|
||||
"ignore_notifications_modal.ignore": "Kā通知忽略",
|
||||
"ignore_notifications_modal.limited_accounts_title": "Kám beh忽略受限制ê口座送來ê通知?",
|
||||
"ignore_notifications_modal.new_accounts_title": "Kám beh忽略新口座送來ê通知?",
|
||||
"ignore_notifications_modal.not_followers_title": "Kám beh忽略無跟tuè lí ê口座送來ê通知?",
|
||||
"ignore_notifications_modal.not_following_title": "Kám beh忽略lí 無跟tuè ê口座送來ê通知?",
|
||||
"ignore_notifications_modal.private_mentions_title": "忽略ka-kī主動送ê私人提起ê通知?",
|
||||
"info_button.label": "幫tsān",
|
||||
"info_button.what_is_alt_text": "<h1>Siánn物是替代文字?</h1> <p>替代文字kā視覺有障礙、網路速度khah慢,á是beh tshuē頂下文ê lâng,提供圖ê敘述。</p> <p>Lí ē當通過寫明白、簡單kap客觀ê替代文字,替逐家改善容易使用性kap幫tsān理解。</p> <ul> <li>掌握重要ê因素</li> <li>替圖寫摘要ê文字</li> <li>用規則ê語句結構</li> <li>避免重複ê資訊</li> <li>專注佇趨勢kap佇複雜視覺(比如圖表á是地圖)內底tshuē關鍵</li> </ul>",
|
||||
"interaction_modal.action.favourite": "Nā beh繼續,lí tio̍h用你ê口座收藏。",
|
||||
"interaction_modal.action.follow": "Nā beh繼續,lí tio̍h用你ê口座跟tuè。",
|
||||
"interaction_modal.action.reblog": "Nā beh繼續,lí tio̍h用你ê口座轉送。",
|
||||
"interaction_modal.action.reply": "Nā beh繼續,lí tio̍h用你ê口座回應。",
|
||||
"interaction_modal.action.vote": "Nā beh繼續,lí tio̍h用你ê口座投票。",
|
||||
"interaction_modal.go": "行",
|
||||
"interaction_modal.no_account_yet": "Tsit-má iáu bô口座?",
|
||||
"interaction_modal.on_another_server": "佇無kâng ê服侍器",
|
||||
"interaction_modal.on_this_server": "Tī tsit ê服侍器",
|
||||
"interaction_modal.title.favourite": "收藏 {name} ê PO文",
|
||||
"interaction_modal.title.follow": "跟tuè {name}",
|
||||
"interaction_modal.title.reblog": "轉送 {name} ê PO文",
|
||||
"interaction_modal.title.reply": "回應 {name} ê PO文",
|
||||
"interaction_modal.title.vote": "參加 {name} ê投票",
|
||||
"interaction_modal.username_prompt": "比如:{example}",
|
||||
"intervals.full.days": "{number, plural, other {# kang}}",
|
||||
"intervals.full.hours": "{number, plural, other {# 點鐘}}",
|
||||
"intervals.full.minutes": "{number, plural, other {# 分鐘}}",
|
||||
"keyboard_shortcuts.back": "Tńg去",
|
||||
"keyboard_shortcuts.blocked": "開封鎖ê用者ê列單",
|
||||
"keyboard_shortcuts.boost": "轉送PO文",
|
||||
"keyboard_shortcuts.column": "揀tsit ê欄",
|
||||
"keyboard_shortcuts.compose": "揀寫文字ê框仔",
|
||||
"keyboard_shortcuts.description": "說明",
|
||||
"keyboard_shortcuts.direct": "phah開私人提起ê欄",
|
||||
"keyboard_shortcuts.down": "佇列單內kā suá khah 下kha",
|
||||
"keyboard_shortcuts.enter": "Phah開PO文",
|
||||
"keyboard_shortcuts.favourite": "收藏PO文",
|
||||
"keyboard_shortcuts.favourites": "Phah開收藏ê列單",
|
||||
"keyboard_shortcuts.federated": "Phah開聯邦ê時間線",
|
||||
"keyboard_shortcuts.heading": "鍵盤ê快速key",
|
||||
"keyboard_shortcuts.home": "Phah開tshù ê時間線",
|
||||
"keyboard_shortcuts.hotkey": "快速key",
|
||||
"keyboard_shortcuts.legend": "顯示tsit篇說明",
|
||||
"keyboard_shortcuts.local": "Phah開本站ê時間線",
|
||||
"keyboard_shortcuts.mention": "提起作者",
|
||||
"keyboard_shortcuts.muted": "Phah開消音ê用者列單",
|
||||
"keyboard_shortcuts.my_profile": "Phah開lí ê個人資料",
|
||||
"keyboard_shortcuts.notifications": "Phah開通知欄",
|
||||
"keyboard_shortcuts.open_media": "Phah開媒體",
|
||||
"keyboard_shortcuts.pinned": "Phah開釘起來ê PO文列單",
|
||||
"keyboard_shortcuts.profile": "Phah開作者ê個人資料",
|
||||
"keyboard_shortcuts.reply": "回應PO文",
|
||||
"keyboard_shortcuts.requests": "Phah開跟tuè請求ê列單",
|
||||
"keyboard_shortcuts.search": "揀tshiau-tshuē條á",
|
||||
"keyboard_shortcuts.spoilers": "顯示/隱藏內容警告",
|
||||
"keyboard_shortcuts.start": "Phah開「開始用」欄",
|
||||
"keyboard_shortcuts.toggle_hidden": "顯示/隱藏內容警告後壁ê PO文",
|
||||
"notification.favourite_pm": "{name} kah意lí ê私人提起",
|
||||
"notification.favourite_pm.name_and_others_with_link": "{name} kap<a>{count, plural, other {另外 # ê lâng}}</a>kah意lí ê私人提起",
|
||||
"search_popout.language_code": "ISO語言代碼",
|
||||
|
|
|
@ -872,7 +872,9 @@
|
|||
"subscribed_languages.target": "Изменить языки подписки для {target}",
|
||||
"tabs_bar.home": "Главная",
|
||||
"tabs_bar.notifications": "Уведомления",
|
||||
"terms_of_service.effective_as_of": "Действует с {date}",
|
||||
"terms_of_service.title": "Пользовательское соглашение",
|
||||
"terms_of_service.upcoming_changes_on": "Предстоящие изменения {date}",
|
||||
"time_remaining.days": "{number, plural, one {остался # день} few {осталось # дня} many {осталось # дней} other {осталось # дней}}",
|
||||
"time_remaining.hours": "{number, plural, one {остался # час} few {осталось # часа} many {осталось # часов} other {осталось # часов}}",
|
||||
"time_remaining.minutes": "{number, plural, one {осталась # минута} few {осталось # минуты} many {осталось # минут} other {осталось # минут}}",
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
"account.statuses_counter": "{count, plural, other {toki {counter}}}",
|
||||
"account.unblock": "o weka ala e jan {name}",
|
||||
"account.unblock_domain": "o weka ala e ma {domain}",
|
||||
"account.unblock_short": "o weka ala",
|
||||
"account.unblock_short": "o pini weka",
|
||||
"account.unendorse": "lipu jan la o suli ala e ni",
|
||||
"account.unfollow": "o kute ala",
|
||||
"account.unmute": "o len ala e @{name}",
|
||||
|
@ -114,8 +114,8 @@
|
|||
"attachments_list.unprocessed": "(nasin open)",
|
||||
"audio.hide": "o len e kalama",
|
||||
"block_modal.remote_users_caveat": "mi pana e wile sina tawa ma {domain}. taso, o sona: ma li ken kepeken nasin len ante la pakala li ken lon. toki pi lukin ale la jan pi ma ala li ken lukin.",
|
||||
"block_modal.show_less": "o lili e lukin",
|
||||
"block_modal.show_more": "o suli e lukin",
|
||||
"block_modal.show_less": "o pana e lili",
|
||||
"block_modal.show_more": "o pana e mute",
|
||||
"block_modal.they_cant_mention": "ona li ken ala toki tawa sina li ken ala kute e sina.",
|
||||
"block_modal.they_cant_see_posts": "ona li ken ala lukin e toki sina. sina ken ala lukin e toki ona.",
|
||||
"block_modal.they_will_know": "ona li sona e ni: sina weka e lukin ona.",
|
||||
|
@ -215,6 +215,9 @@
|
|||
"confirmations.logout.confirm": "o weka",
|
||||
"confirmations.logout.message": "sina wile ala wile weka",
|
||||
"confirmations.logout.title": "o weka?",
|
||||
"confirmations.missing_alt_text.confirm": "pana e toki pi sona lukin",
|
||||
"confirmations.missing_alt_text.message": "toki ni la sitelen li lon. taso toki pi sona lukin li lon ala. toki pi sona lukin li pona tan ni: jan ale li ken sona e toki.",
|
||||
"confirmations.missing_alt_text.title": "o pana e toki pi sona lukin",
|
||||
"confirmations.mute.confirm": "o len",
|
||||
"confirmations.redraft.confirm": "o weka o pali sin e toki",
|
||||
"confirmations.redraft.message": "pali sin e toki ni la sina wile ala wile weka e ona? sina ni la suli pi toki ni en wawa pi toki ni li weka. kin la toki lon toki ni li jo e mama ala.",
|
||||
|
@ -235,6 +238,7 @@
|
|||
"copy_icon_button.copied": "toki li awen lon ilo sina",
|
||||
"copypaste.copied": "sina jo e toki",
|
||||
"copypaste.copy_to_clipboard": "o awen lon ilo sina",
|
||||
"directory.federated": "tan lipu ante sona",
|
||||
"directory.local": "tan {domain} taso",
|
||||
"directory.new_arrivals": "jan pi kama sin",
|
||||
"directory.recently_active": "jan lon tenpo poka",
|
||||
|
@ -243,6 +247,7 @@
|
|||
"dismissable_banner.community_timeline": "ni li toki pi tenpo poka tawa ale tan jan lon ma lawa pi nimi {domain}.",
|
||||
"dismissable_banner.dismiss": "o weka",
|
||||
"dismissable_banner.explore_links": "tenpo suno ni la jan pi kulupu ale li toki e ijo sin ni. ijo sin pi jan ante mute li sewi lon lipu ni.",
|
||||
"dismissable_banner.explore_statuses": "jan mute li lukin e toki ni tan ma ilo weka. toki sin en toki pi wawa mute li lon sewi.",
|
||||
"domain_block_modal.block": "o weka e ma",
|
||||
"domain_block_modal.they_wont_know": "ona li sona ala e ni: sina weka e ona.",
|
||||
"domain_block_modal.title": "sina wile weka ala weka e ma?",
|
||||
|
@ -276,6 +281,7 @@
|
|||
"emoji_button.symbols": "sitelen",
|
||||
"emoji_button.travel": "ma en tawa",
|
||||
"empty_column.account_hides_collections": "jan ni li wile len e sona ni",
|
||||
"empty_column.account_suspended": "lipu ni li weka",
|
||||
"empty_column.account_timeline": "toki ala li lon!",
|
||||
"empty_column.account_unavailable": "ken ala lukin e lipu jan",
|
||||
"empty_column.blocks": "jan ala li weka tawa sina.",
|
||||
|
@ -328,6 +334,7 @@
|
|||
"hashtag.counter_by_uses": "{count, plural, other {toki {counter}}}",
|
||||
"hashtag.follow": "o kute e kulupu lipu",
|
||||
"hashtag.unfollow": "o kute ala e kulupu lipu",
|
||||
"home.column_settings.show_reblogs": "lukin e wawa",
|
||||
"home.pending_critical_update.link": "o lukin e ijo ilo sin",
|
||||
"info_button.label": "sona",
|
||||
"interaction_modal.go": "o tawa ma ni",
|
||||
|
@ -376,6 +383,7 @@
|
|||
"navigation_bar.about": "sona",
|
||||
"navigation_bar.blocks": "jan weka",
|
||||
"navigation_bar.compose": "o pali e toki sin",
|
||||
"navigation_bar.domain_blocks": "kulupu pi ma weka",
|
||||
"navigation_bar.favourites": "ijo pona",
|
||||
"navigation_bar.filters": "nimi len",
|
||||
"navigation_bar.lists": "kulupu lipu",
|
||||
|
@ -472,6 +480,8 @@
|
|||
"status.pin": "o sewi lon lipu sina",
|
||||
"status.pinned": "toki sewi",
|
||||
"status.reblog": "o wawa",
|
||||
"status.reblogged_by": "jan {name} li wawa",
|
||||
"status.reblogs.empty": "jan ala li wawa e toki ni. jan li wawa la, nimi ona li sitelen lon ni.",
|
||||
"status.share": "o pana tawa ante",
|
||||
"status.show_less_all": "o lili e ale",
|
||||
"status.show_more_all": "o suli e ale",
|
||||
|
|
|
@ -872,7 +872,9 @@
|
|||
"subscribed_languages.target": "更改 {target} 的订阅语言",
|
||||
"tabs_bar.home": "主页",
|
||||
"tabs_bar.notifications": "通知",
|
||||
"terms_of_service.effective_as_of": "自 {date} 起生效",
|
||||
"terms_of_service.title": "服务条款",
|
||||
"terms_of_service.upcoming_changes_on": "将于 {date} 进行变更",
|
||||
"time_remaining.days": "剩余 {number, plural, other {# 天}}",
|
||||
"time_remaining.hours": "剩余 {number, plural, other {# 小时}}",
|
||||
"time_remaining.minutes": "剩余 {number, plural, other {# 分钟}}",
|
||||
|
|
14
app/javascript/mastodon/models/alert.ts
Normal file
14
app/javascript/mastodon/models/alert.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import type { MessageDescriptor } from 'react-intl';
|
||||
|
||||
export type TranslatableString = string | MessageDescriptor;
|
||||
|
||||
export type TranslatableValues = Record<string, string | number | Date>;
|
||||
|
||||
export interface Alert {
|
||||
key: number;
|
||||
title?: TranslatableString;
|
||||
message: TranslatableString;
|
||||
action?: TranslatableString;
|
||||
values?: TranslatableValues;
|
||||
onClick?: () => void;
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import {
|
||||
ALERT_SHOW,
|
||||
ALERT_DISMISS,
|
||||
ALERT_CLEAR,
|
||||
} from '../actions/alerts';
|
||||
|
||||
const initialState = ImmutableList([]);
|
||||
|
||||
let id = 0;
|
||||
|
||||
const addAlert = (state, alert) =>
|
||||
state.push({
|
||||
key: id++,
|
||||
...alert,
|
||||
});
|
||||
|
||||
export default function alerts(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case ALERT_SHOW:
|
||||
return addAlert(state, action.alert);
|
||||
case ALERT_DISMISS:
|
||||
return state.filterNot(item => item.key === action.alert.key);
|
||||
case ALERT_CLEAR:
|
||||
return state.clear();
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
24
app/javascript/mastodon/reducers/alerts.ts
Normal file
24
app/javascript/mastodon/reducers/alerts.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { createReducer } from '@reduxjs/toolkit';
|
||||
|
||||
import { showAlert, dismissAlert, clearAlerts } from 'mastodon/actions/alerts';
|
||||
import type { Alert } from 'mastodon/models/alert';
|
||||
|
||||
const initialState: Alert[] = [];
|
||||
|
||||
let id = 0;
|
||||
|
||||
export const alertsReducer = createReducer(initialState, (builder) => {
|
||||
builder
|
||||
.addCase(showAlert, (state, { payload }) => {
|
||||
state.push({
|
||||
key: id++,
|
||||
...payload,
|
||||
});
|
||||
})
|
||||
.addCase(dismissAlert, (state, { payload: { key } }) => {
|
||||
return state.filter((item) => item.key !== key);
|
||||
})
|
||||
.addCase(clearAlerts, () => {
|
||||
return [];
|
||||
});
|
||||
});
|
|
@ -5,7 +5,7 @@ import { combineReducers } from 'redux-immutable';
|
|||
|
||||
import { accountsReducer } from './accounts';
|
||||
import accounts_map from './accounts_map';
|
||||
import alerts from './alerts';
|
||||
import { alertsReducer } from './alerts';
|
||||
import announcements from './announcements';
|
||||
import { antennasReducer } from './antennas';
|
||||
import { bookmarkCategoriesReducer } from './bookmark_categories';
|
||||
|
@ -49,7 +49,7 @@ const reducers = {
|
|||
dropdownMenu: dropdownMenuReducer,
|
||||
timelines,
|
||||
meta,
|
||||
alerts,
|
||||
alerts: alertsReducer,
|
||||
loadingBar: loadingBarReducer,
|
||||
modal: modalReducer,
|
||||
user_lists,
|
||||
|
|
|
@ -17,9 +17,10 @@ export const makeGetStatus = () => {
|
|||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', id, 'account'])]),
|
||||
(state, { id }) => state.getIn(['accounts', state.getIn(['statuses', state.getIn(['statuses', id, 'reblog']), 'account'])]),
|
||||
getFilters,
|
||||
(_, { contextType }) => ['detailed', 'bookmarks', 'favourites'].includes(contextType),
|
||||
],
|
||||
|
||||
(statusBase, statusReblog, statusQuote, statusReblogQuote, accountBase, accountReblog, filters) => {
|
||||
(statusBase, statusReblog, statusQuote, statusReblogQuote, accountBase, accountReblog, filters, warnInsteadOfHide) => {
|
||||
if (!statusBase || statusBase.get('isLoading')) {
|
||||
return null;
|
||||
}
|
||||
|
@ -36,6 +37,7 @@ export const makeGetStatus = () => {
|
|||
}
|
||||
|
||||
let filtered = false;
|
||||
let mediaFiltered = false;
|
||||
if ((accountReblog || accountBase).get('id') !== me && filters) {
|
||||
let filterResults = statusReblog?.get('filtered') || statusBase.get('filtered') || ImmutableList();
|
||||
const quoteFilterResults = statusQuote?.get('filtered');
|
||||
|
@ -46,10 +48,16 @@ export const makeGetStatus = () => {
|
|||
}
|
||||
}
|
||||
|
||||
if (filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
||||
if (!warnInsteadOfHide && filterResults.some((result) => filters.getIn([result.get('filter'), 'filter_action']) === 'hide')) {
|
||||
return null;
|
||||
}
|
||||
filterResults = filterResults.filter(result => filters.has(result.get('filter')));
|
||||
|
||||
let mediaFilters = filterResults.filter(result => filters.getIn([result.get('filter'), 'filter_action']) === 'blur');
|
||||
if (!mediaFilters.isEmpty()) {
|
||||
mediaFiltered = mediaFilters.map(result => filters.getIn([result.get('filter'), 'title']));
|
||||
}
|
||||
|
||||
filterResults = filterResults.filter(result => filters.has(result.get('filter')) && filters.getIn([result.get('filter'), 'filter_action']) !== 'blur');
|
||||
if (!filterResults.isEmpty()) {
|
||||
filtered = filterResults.map(result => filters.getIn([result.get('filter'), 'title']));
|
||||
}
|
||||
|
@ -60,6 +68,7 @@ export const makeGetStatus = () => {
|
|||
map.set('quote', statusQuote);
|
||||
map.set('account', accountBase);
|
||||
map.set('matched_filters', filtered);
|
||||
map.set('matched_media_filters', mediaFiltered);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
@ -75,28 +84,6 @@ export const makeGetPictureInPicture = () => {
|
|||
}));
|
||||
};
|
||||
|
||||
const ALERT_DEFAULTS = {
|
||||
dismissAfter: 5000,
|
||||
style: false,
|
||||
};
|
||||
|
||||
const formatIfNeeded = (intl, message, values) => {
|
||||
if (typeof message === 'object') {
|
||||
return intl.formatMessage(message, values);
|
||||
}
|
||||
|
||||
return message;
|
||||
};
|
||||
|
||||
export const getAlerts = createSelector([state => state.get('alerts'), (_, { intl }) => intl], (alerts, intl) =>
|
||||
alerts.map(item => ({
|
||||
...ALERT_DEFAULTS,
|
||||
...item,
|
||||
action: formatIfNeeded(intl, item.action, item.values),
|
||||
title: formatIfNeeded(intl, item.title, item.values),
|
||||
message: formatIfNeeded(intl, item.message, item.values),
|
||||
})).toArray());
|
||||
|
||||
export const makeGetNotification = () => createSelector([
|
||||
(_, base) => base,
|
||||
(state, _, accountId) => state.getIn(['accounts', accountId]),
|
||||
|
|
|
@ -12,19 +12,21 @@ import type { AsyncThunkRejectValue } from '../typed_functions';
|
|||
const defaultFailSuffix = 'FAIL';
|
||||
const isFailedAction = new RegExp(`${defaultFailSuffix}$`, 'g');
|
||||
|
||||
interface ActionWithMaybeAlertParams extends Action, AsyncThunkRejectValue {}
|
||||
|
||||
interface RejectedAction extends Action {
|
||||
payload: AsyncThunkRejectValue;
|
||||
}
|
||||
|
||||
interface ActionWithMaybeAlertParams extends Action, AsyncThunkRejectValue {
|
||||
payload?: AsyncThunkRejectValue;
|
||||
}
|
||||
|
||||
function isRejectedActionWithPayload(
|
||||
action: unknown,
|
||||
): action is RejectedAction {
|
||||
return isAsyncThunkAction(action) && isRejectedWithValue(action);
|
||||
}
|
||||
|
||||
function isActionWithmaybeAlertParams(
|
||||
function isActionWithMaybeAlertParams(
|
||||
action: unknown,
|
||||
): action is ActionWithMaybeAlertParams {
|
||||
return isAction(action);
|
||||
|
@ -40,11 +42,12 @@ export const errorsMiddleware: Middleware<{}, RootState> =
|
|||
showAlertForError(action.payload.error, action.payload.skipNotFound),
|
||||
);
|
||||
} else if (
|
||||
isActionWithmaybeAlertParams(action) &&
|
||||
!action.skipAlert &&
|
||||
isActionWithMaybeAlertParams(action) &&
|
||||
!(action.payload?.skipAlert || action.skipAlert) &&
|
||||
action.type.match(isFailedAction)
|
||||
) {
|
||||
dispatch(showAlertForError(action.error, action.skipNotFound));
|
||||
const { error, skipNotFound } = action.payload ?? action;
|
||||
dispatch(showAlertForError(error, skipNotFound));
|
||||
}
|
||||
|
||||
return next(action);
|
||||
|
|
|
@ -7,6 +7,11 @@ export const toServerSideType = (columnType: string) => {
|
|||
case 'account':
|
||||
case 'explore':
|
||||
return columnType;
|
||||
case 'detailed':
|
||||
return 'thread';
|
||||
case 'bookmarks':
|
||||
case 'favourites':
|
||||
return 'home';
|
||||
default:
|
||||
if (columnType.includes('list:') || columnType.includes('antenna:')) {
|
||||
return 'home';
|
||||
|
|
|
@ -1,25 +1,27 @@
|
|||
@import 'mastodon/mixins';
|
||||
@import 'mastodon/variables';
|
||||
@import 'fonts/roboto';
|
||||
@import 'fonts/roboto-mono';
|
||||
@use 'mastodon/functions';
|
||||
@use 'mastodon/mixins';
|
||||
@use 'mastodon/variables';
|
||||
@use 'mastodon/css_variables';
|
||||
@use 'fonts/roboto';
|
||||
@use 'fonts/roboto-mono';
|
||||
|
||||
@import 'mastodon/reset';
|
||||
@import 'mastodon/basics';
|
||||
@import 'mastodon/branding';
|
||||
@import 'mastodon/containers';
|
||||
@import 'mastodon/lists';
|
||||
@import 'mastodon/widgets';
|
||||
@import 'mastodon/forms';
|
||||
@import 'mastodon/accounts';
|
||||
@import 'mastodon/components';
|
||||
@import 'mastodon/polls';
|
||||
@import 'mastodon/modal';
|
||||
@import 'mastodon/emoji_picker';
|
||||
@import 'mastodon/annual_reports';
|
||||
@import 'mastodon/about';
|
||||
@import 'mastodon/tables';
|
||||
@import 'mastodon/admin';
|
||||
@import 'mastodon/dashboard';
|
||||
@import 'mastodon/rtl';
|
||||
@import 'mastodon/accessibility';
|
||||
@import 'mastodon/rich_text';
|
||||
@use 'mastodon/reset';
|
||||
@use 'mastodon/basics';
|
||||
@use 'mastodon/branding';
|
||||
@use 'mastodon/containers';
|
||||
@use 'mastodon/lists';
|
||||
@use 'mastodon/widgets';
|
||||
@use 'mastodon/forms';
|
||||
@use 'mastodon/accounts';
|
||||
@use 'mastodon/components';
|
||||
@use 'mastodon/polls';
|
||||
@use 'mastodon/modal';
|
||||
@use 'mastodon/emoji_picker';
|
||||
@use 'mastodon/annual_reports';
|
||||
@use 'mastodon/about';
|
||||
@use 'mastodon/tables';
|
||||
@use 'mastodon/admin';
|
||||
@use 'mastodon/dashboard';
|
||||
@use 'mastodon/rtl';
|
||||
@use 'mastodon/accessibility';
|
||||
@use 'mastodon/rich_text';
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
@import 'contrast/variables';
|
||||
@import 'application';
|
||||
@import 'contrast/diff';
|
||||
@use 'contrast/variables';
|
||||
@use 'application';
|
||||
@use 'contrast/diff';
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@use '../mastodon/variables' as *;
|
||||
|
||||
.status__content a,
|
||||
.reply-indicator__content a,
|
||||
.edit-indicator__content a,
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@use '../mastodon/functions' as *;
|
||||
|
||||
// Dependent colors
|
||||
$black: #000000;
|
||||
|
||||
|
@ -14,12 +16,13 @@ $ui-primary-color: $classic-primary-color !default;
|
|||
$ui-secondary-color: $classic-secondary-color !default;
|
||||
$ui-highlight-color: $classic-highlight-color !default;
|
||||
|
||||
$darker-text-color: lighten($ui-primary-color, 20%) !default;
|
||||
$dark-text-color: lighten($ui-primary-color, 12%) !default;
|
||||
$secondary-text-color: lighten($ui-secondary-color, 6%) !default;
|
||||
$highlight-text-color: lighten($ui-highlight-color, 10%) !default;
|
||||
$action-button-color: lighten($ui-base-color, 50%);
|
||||
|
||||
$inverted-text-color: $black !default;
|
||||
$lighter-text-color: darken($ui-base-color, 6%) !default;
|
||||
$light-text-color: darken($ui-primary-color, 40%) !default;
|
||||
@use '../mastodon/variables' with (
|
||||
$darker-text-color: lighten($ui-primary-color, 20%),
|
||||
$dark-text-color: lighten($ui-primary-color, 12%),
|
||||
$secondary-text-color: lighten($ui-secondary-color, 6%),
|
||||
$highlight-text-color: lighten($ui-highlight-color, 10%),
|
||||
$action-button-color: lighten($ui-base-color, 50%),
|
||||
$inverted-text-color: $black,
|
||||
$lighter-text-color: darken($ui-base-color, 6%),
|
||||
$light-text-color: darken($ui-primary-color, 40%)
|
||||
);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@import 'full-dark/variables';
|
||||
@import 'application';
|
||||
@import 'full-dark/diff';
|
||||
@use 'full-dark/variables';
|
||||
@use 'full-dark/css_variables';
|
||||
@use 'application';
|
||||
@use 'full-dark/diff';
|
||||
|
|
4
app/javascript/styles/full-dark/css_variables.scss
Normal file
4
app/javascript/styles/full-dark/css_variables.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
@use 'sass:color';
|
||||
@use '../mastodon/variables' as *;
|
||||
@use 'variables' as *;
|
||||
@use '../mastodon/functions' as *;
|
|
@ -1,3 +1,7 @@
|
|||
@use 'sass:color';
|
||||
@use '../mastodon/functions' as *;
|
||||
@use '../mastodon/variables' as *;
|
||||
|
||||
input[type='text']:not(#cw-spoiler-input),
|
||||
input[type='search'],
|
||||
input[type='number'],
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import 'fonts/inter';
|
||||
@use 'fonts/inter';
|
||||
|
||||
body {
|
||||
accent-color: #6364ff;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@import 'mastodon-light/variables';
|
||||
@import 'application';
|
||||
@import 'mastodon-light/diff';
|
||||
@use 'mastodon-light/variables';
|
||||
@use 'mastodon-light/css_variables';
|
||||
@use 'application';
|
||||
@use 'mastodon-light/diff';
|
||||
|
|
21
app/javascript/styles/mastodon-light/css_variables.scss
Normal file
21
app/javascript/styles/mastodon-light/css_variables.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
@use 'sass:color';
|
||||
@use '../mastodon/variables' as *;
|
||||
@use 'variables' as *;
|
||||
@use '../mastodon/functions' as *;
|
||||
|
||||
body {
|
||||
--dropdown-border-color: hsl(240deg, 25%, 88%);
|
||||
--dropdown-background-color: #fff;
|
||||
--modal-border-color: hsl(240deg, 25%, 88%);
|
||||
--modal-background-color: var(--background-color-tint);
|
||||
--background-border-color: hsl(240deg, 25%, 88%);
|
||||
--background-color: #fff;
|
||||
--background-color-tint: rgba(255, 255, 255, 80%);
|
||||
--background-filter: blur(10px);
|
||||
--on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.65)};
|
||||
--rich-text-container-color: rgba(255, 216, 231, 100%);
|
||||
--rich-text-text-color: rgba(114, 47, 83, 100%);
|
||||
--rich-text-decorations-color: rgba(255, 175, 212, 100%);
|
||||
--input-placeholder-color: #{color.adjust($dark-text-color, $alpha: -0.5)};
|
||||
--input-background-color: #{darken($ui-base-color, 10%)};
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
// Notes!
|
||||
// Sass color functions, "darken" and "lighten" are automatically replaced.
|
||||
@use 'sass:color';
|
||||
@use '../mastodon/functions' as *;
|
||||
@use '../mastodon/variables' as *;
|
||||
|
||||
.simple_form .button.button-tertiary {
|
||||
color: $highlight-text-color;
|
||||
|
@ -152,8 +155,12 @@
|
|||
}
|
||||
|
||||
.reactions-bar__item.active {
|
||||
background-color: mix($white, $ui-highlight-color, 80%);
|
||||
border-color: mix(lighten($ui-base-color, 8%), $ui-highlight-color, 80%);
|
||||
background-color: color.mix($white, $ui-highlight-color, 80%);
|
||||
border-color: color.mix(
|
||||
lighten($ui-base-color, 8%),
|
||||
$ui-highlight-color,
|
||||
80%
|
||||
);
|
||||
}
|
||||
|
||||
.media-modal__overlay .picture-in-picture__footer {
|
||||
|
@ -242,7 +249,7 @@
|
|||
|
||||
// Change the default colors used on some parts of the profile pages
|
||||
.activity-stream-tabs {
|
||||
background: $account-background-color;
|
||||
background: $white;
|
||||
border-bottom-color: lighten($ui-base-color, 8%);
|
||||
}
|
||||
|
||||
|
@ -284,7 +291,7 @@
|
|||
}
|
||||
|
||||
.entry {
|
||||
background: $account-background-color;
|
||||
background: $white;
|
||||
|
||||
.detailed-status.light,
|
||||
.more.light,
|
||||
|
|
|
@ -1,87 +1,49 @@
|
|||
@use 'sass:color';
|
||||
|
||||
// Dependent colors
|
||||
$black: #000000;
|
||||
$white: #ffffff;
|
||||
@use '../mastodon/functions' with (
|
||||
$darken-multiplier: 1,
|
||||
$lighten-multiplier: -1
|
||||
);
|
||||
|
||||
$classic-base-color: hsl(240deg, 16%, 19%);
|
||||
$classic-primary-color: hsl(240deg, 29%, 70%);
|
||||
$classic-secondary-color: hsl(255deg, 25%, 88%);
|
||||
$classic-highlight-color: hsl(240deg, 100%, 69%);
|
||||
|
||||
$blurple-600: hsl(252deg, 59%, 51%); // Iris
|
||||
$blurple-500: hsl(240deg, 100%, 69%); // Brand purple
|
||||
$blurple-300: hsl(237deg, 92%, 75%); // Faded Blue
|
||||
$black: #000000; // Black
|
||||
$white: #ffffff; // White
|
||||
$blurple-500: #6364ff; // Brand purple
|
||||
$grey-600: hsl(240deg, 8%, 33%); // Trout
|
||||
$grey-100: hsl(240deg, 51%, 90%); // Topaz
|
||||
|
||||
$emoji-reaction-color: #dfe5f5 !default;
|
||||
$emoji-reaction-selected-color: #9ac1f2 !default;
|
||||
|
||||
// Differences
|
||||
$success-green: lighten(hsl(138deg, 32%, 35%), 8%);
|
||||
$classic-base-color: hsl(240deg, 16%, 19%);
|
||||
$classic-secondary-color: hsl(255deg, 25%, 88%);
|
||||
$classic-highlight-color: $blurple-500;
|
||||
|
||||
$base-overlay-background: $white !default;
|
||||
$valid-value-color: $success-green !default;
|
||||
@use '../mastodon/variables' with (
|
||||
$success-green: color.adjust(
|
||||
hsl(138deg, 32%, 35%),
|
||||
$lightness: 8%,
|
||||
$space: hsl
|
||||
),
|
||||
$base-overlay-background: $white,
|
||||
|
||||
$ui-base-color: $classic-secondary-color !default;
|
||||
$ui-base-lighter-color: hsl(250deg, 24%, 75%);
|
||||
$ui-primary-color: $classic-primary-color !default;
|
||||
$ui-secondary-color: $classic-base-color !default;
|
||||
$ui-highlight-color: $classic-highlight-color !default;
|
||||
$ui-base-color: $classic-secondary-color,
|
||||
$ui-base-lighter-color: hsl(250deg, 24%, 75%),
|
||||
$ui-secondary-color: $classic-base-color,
|
||||
|
||||
$ui-button-secondary-color: $grey-600 !default;
|
||||
$ui-button-secondary-border-color: $grey-600 !default;
|
||||
$ui-button-secondary-focus-color: $white !default;
|
||||
$ui-button-secondary-color: $grey-600,
|
||||
$ui-button-secondary-border-color: $grey-600,
|
||||
$ui-button-secondary-focus-color: $white,
|
||||
$ui-button-tertiary-color: $blurple-500,
|
||||
$ui-button-tertiary-border-color: $blurple-500,
|
||||
|
||||
$ui-button-tertiary-color: $blurple-500 !default;
|
||||
$ui-button-tertiary-border-color: $blurple-500 !default;
|
||||
$primary-text-color: $black,
|
||||
$darker-text-color: $classic-base-color,
|
||||
$lighter-text-color: $classic-base-color,
|
||||
$highlight-text-color: $classic-highlight-color,
|
||||
$dark-text-color: hsl(240deg, 16%, 32%),
|
||||
$light-text-color: hsl(240deg, 16%, 32%),
|
||||
$inverted-text-color: $black,
|
||||
|
||||
$primary-text-color: $black !default;
|
||||
$darker-text-color: $classic-base-color !default;
|
||||
$highlight-text-color: $ui-highlight-color !default;
|
||||
$dark-text-color: hsl(240deg, 16%, 32%);
|
||||
$action-button-color: hsl(240deg, 16%, 45%);
|
||||
|
||||
$inverted-text-color: $black !default;
|
||||
$lighter-text-color: $classic-base-color !default;
|
||||
$light-text-color: hsl(240deg, 16%, 32%);
|
||||
|
||||
// Newly added colors
|
||||
$account-background-color: $white !default;
|
||||
|
||||
// Invert darkened and lightened colors
|
||||
@function darken($color, $amount) {
|
||||
@return hsl(
|
||||
hue($color),
|
||||
color.channel($color, 'saturation', $space: hsl),
|
||||
color.channel($color, 'lightness', $space: hsl) + $amount
|
||||
);
|
||||
}
|
||||
|
||||
@function lighten($color, $amount) {
|
||||
@return hsl(
|
||||
hue($color),
|
||||
color.channel($color, 'saturation', $space: hsl),
|
||||
color.channel($color, 'lightness', $space: hsl) - $amount
|
||||
);
|
||||
}
|
||||
|
||||
$emojis-requiring-inversion: 'chains';
|
||||
|
||||
body {
|
||||
--dropdown-border-color: hsl(240deg, 25%, 88%);
|
||||
--dropdown-background-color: #fff;
|
||||
--modal-border-color: hsl(240deg, 25%, 88%);
|
||||
--modal-background-color: var(--background-color-tint);
|
||||
--background-border-color: hsl(240deg, 25%, 88%);
|
||||
--background-color: #fff;
|
||||
--background-color-tint: rgba(255, 255, 255, 80%);
|
||||
--background-filter: blur(10px);
|
||||
--on-surface-color: #{transparentize($ui-base-color, 0.65)};
|
||||
--rich-text-container-color: rgba(255, 216, 231, 100%);
|
||||
--rich-text-text-color: rgba(114, 47, 83, 100%);
|
||||
--rich-text-decorations-color: rgba(255, 175, 212, 100%);
|
||||
--input-placeholder-color: #{transparentize($dark-text-color, 0.5)};
|
||||
--input-background-color: #{darken($ui-base-color, 10%)};
|
||||
}
|
||||
$action-button-color: hsl(240deg, 16%, 45%),
|
||||
$emojis-requiring-inversion: 'chains'
|
||||
);
|
||||
|
|
21
app/javascript/styles/mastodon/_functions.scss
Normal file
21
app/javascript/styles/mastodon/_functions.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
@use 'sass:color';
|
||||
|
||||
$darken-multiplier: -1 !default;
|
||||
$lighten-multiplier: 1 !default;
|
||||
|
||||
// Invert darkened and lightened colors
|
||||
@function darken($color, $amount) {
|
||||
@return color.adjust(
|
||||
$color,
|
||||
$lightness: $amount * $darken-multiplier,
|
||||
$space: hsl
|
||||
);
|
||||
}
|
||||
|
||||
@function lighten($color, $amount) {
|
||||
@return color.adjust(
|
||||
$color,
|
||||
$lightness: $amount * $lighten-multiplier,
|
||||
$space: hsl
|
||||
);
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
@use 'variables' as *;
|
||||
|
||||
@mixin search-input {
|
||||
outline: 0;
|
||||
box-sizing: border-box;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@use 'sass:color';
|
||||
@use 'functions' as *;
|
||||
|
||||
// Commonly used web colors
|
||||
$black: #000000; // Black
|
||||
|
@ -101,43 +102,13 @@ $media-modal-media-max-height: 80%;
|
|||
|
||||
$no-gap-breakpoint: 1175px;
|
||||
$mobile-breakpoint: 630px;
|
||||
$no-columns-breakpoint: 600px;
|
||||
|
||||
$font-sans-serif: 'mastodon-font-sans-serif' !default;
|
||||
$font-display: 'mastodon-font-display' !default;
|
||||
$font-monospace: 'mastodon-font-monospace' !default;
|
||||
|
||||
:root {
|
||||
--dropdown-border-color: #{lighten($ui-base-color, 4%)};
|
||||
--dropdown-background-color: #{rgba(darken($ui-base-color, 8%), 0.9)};
|
||||
--dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)},
|
||||
0 8px 10px -6px #{rgba($base-shadow-color, 0.25)};
|
||||
--modal-background-color: #{rgba(darken($ui-base-color, 8%), 0.7)};
|
||||
--modal-background-variant-color: #{rgba($ui-base-color, 0.7)};
|
||||
--modal-border-color: #{lighten($ui-base-color, 4%)};
|
||||
--background-border-color: #{lighten($ui-base-color, 4%)};
|
||||
--background-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
|
||||
--background-color: #{darken($ui-base-color, 8%)};
|
||||
--background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)};
|
||||
--surface-background-color: #{darken($ui-base-color, 4%)};
|
||||
--surface-variant-background-color: #{$ui-base-color};
|
||||
--surface-variant-active-background-color: #{lighten($ui-base-color, 4%)};
|
||||
--on-surface-color: #{transparentize($ui-base-color, 0.5)};
|
||||
--avatar-border-radius: 8px;
|
||||
--content-font-size: 15px;
|
||||
--content-emoji-size: 20px;
|
||||
--content-line-height: 22px;
|
||||
--detail-content-font-size: 19px;
|
||||
--detail-content-emoji-size: 24px;
|
||||
--detail-content-line-height: 24px;
|
||||
--media-outline-color: #{rgba(#fcf8ff, 0.15)};
|
||||
--overlay-icon-shadow: drop-shadow(0 0 8px #{rgba($base-shadow-color, 0.25)});
|
||||
--error-background-color: #{darken($error-red, 16%)};
|
||||
--error-active-background-color: #{darken($error-red, 12%)};
|
||||
--on-error-color: #fff;
|
||||
--rich-text-container-color: rgba(87, 24, 60, 100%);
|
||||
--rich-text-text-color: rgba(255, 175, 212, 100%);
|
||||
--rich-text-decorations-color: rgba(128, 58, 95, 100%);
|
||||
--input-placeholder-color: #{$dark-text-color};
|
||||
--input-background-color: var(--surface-variant-background-color);
|
||||
--on-input-color: #{$secondary-text-color};
|
||||
}
|
||||
$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange'
|
||||
'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign'
|
||||
'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on'
|
||||
'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default;
|
|
@ -1,3 +1,5 @@
|
|||
@use 'variables' as *;
|
||||
|
||||
$maximum-width: 1235px;
|
||||
$fluid-breakpoint: $maximum-width + 20px;
|
||||
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
$emojis-requiring-inversion: 'back' 'copyright' 'curly_loop' 'currency_exchange'
|
||||
'end' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign'
|
||||
'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'on'
|
||||
'registered' 'soon' 'spider' 'telephone_receiver' 'tm' 'top' 'wavy_dash' !default;
|
||||
@use 'variables' as *;
|
||||
|
||||
%emoji-color-inversion {
|
||||
filter: invert(1);
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
@use 'variables' as *;
|
||||
@use 'functions' as *;
|
||||
|
||||
.card {
|
||||
& > a {
|
||||
display: block;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
@use 'sass:math';
|
||||
@use 'functions' as *;
|
||||
@use 'variables' as *;
|
||||
|
||||
$no-columns-breakpoint: 890px;
|
||||
$sidebar-width: 300px;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@use 'variables' as *;
|
||||
|
||||
:root {
|
||||
--indigo-1: #17063b;
|
||||
--indigo-2: #2f0c7a;
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
@use 'variables' as *;
|
||||
@use 'functions' as *;
|
||||
|
||||
@function hex-color($color) {
|
||||
@if type-of($color) == 'color' {
|
||||
$color: str-slice(ie-hex-str($color), 4);
|
||||
|
@ -92,6 +95,7 @@ body {
|
|||
|
||||
&.with-modals--active {
|
||||
overflow-y: hidden;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@use 'variables' as *;
|
||||
|
||||
.logo {
|
||||
color: $primary-text-color;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
@use 'sass:color';
|
||||
@use 'variables' as *;
|
||||
@use 'functions' as *;
|
||||
@use 'mixins' as *;
|
||||
|
||||
.app-body {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
|
@ -1911,18 +1916,22 @@ body > [data-popper-placement] {
|
|||
.detailed-status__wrapper-direct {
|
||||
.detailed-status,
|
||||
.detailed-status__action-bar {
|
||||
background: mix($ui-base-color, $ui-highlight-color, 95%);
|
||||
background: color.mix($ui-base-color, $ui-highlight-color, 95%);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.detailed-status,
|
||||
.detailed-status__action-bar {
|
||||
background: mix(lighten($ui-base-color, 4%), $ui-highlight-color, 95%);
|
||||
background: color.mix(
|
||||
lighten($ui-base-color, 4%),
|
||||
$ui-highlight-color,
|
||||
95%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.detailed-status__action-bar {
|
||||
border-top-color: mix(
|
||||
border-top-color: color.mix(
|
||||
lighten($ui-base-color, 8%),
|
||||
$ui-highlight-color,
|
||||
95%
|
||||
|
@ -2528,49 +2537,6 @@ a.account__display-name {
|
|||
}
|
||||
}
|
||||
|
||||
.image-loader {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE 10+ */
|
||||
|
||||
* {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE 10+ */
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar,
|
||||
*::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: transparent; /* Chrome/Safari/Webkit */
|
||||
}
|
||||
|
||||
.image-loader__preview-canvas {
|
||||
max-width: $media-modal-media-max-width;
|
||||
max-height: $media-modal-media-max-height;
|
||||
background: url('../images/void.png') repeat;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.loading-bar__container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-bar {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
&.image-loader--amorphous .image-loader__preview-canvas {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.zoomable-image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
@ -2578,13 +2544,61 @@ a.account__display-name {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
scrollbar-width: none;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
|
||||
img {
|
||||
max-width: $media-modal-media-max-width;
|
||||
max-height: $media-modal-media-max-height;
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
outline: 1px solid var(--media-outline-color);
|
||||
outline-offset: -1px;
|
||||
border-radius: 8px;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
&--zoomed-in {
|
||||
z-index: 9999;
|
||||
cursor: grab;
|
||||
|
||||
img {
|
||||
outline: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&--dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
&--error img {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&__preview {
|
||||
max-width: $media-modal-media-max-width;
|
||||
max-height: $media-modal-media-max-height;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
outline: 1px solid var(--media-outline-color);
|
||||
outline-offset: -1px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
canvas {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
z-index: 2;
|
||||
mix-blend-mode: luminosity;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5887,6 +5901,7 @@ a.status-card {
|
|||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
.modal-root__modal {
|
||||
|
@ -6020,7 +6035,7 @@ a.status-card {
|
|||
.picture-in-picture__footer {
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
padding: 20px 0;
|
||||
padding: 16px;
|
||||
|
||||
.icon-button {
|
||||
color: $white;
|
||||
|
@ -7235,6 +7250,10 @@ a.status-card {
|
|||
filter: var(--overlay-icon-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
&--error img {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.media-gallery__item-thumbnail {
|
||||
|
@ -8858,7 +8877,7 @@ noscript {
|
|||
&.active {
|
||||
transition: all 100ms ease-in;
|
||||
transition-property: background-color, color;
|
||||
background-color: mix(
|
||||
background-color: color.mix(
|
||||
lighten($ui-base-color, 12%),
|
||||
$ui-highlight-color,
|
||||
80%
|
||||
|
@ -9759,6 +9778,7 @@ noscript {
|
|||
border: 1px solid $highlight-text-color;
|
||||
background: rgba($highlight-text-color, 0.15);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
|
||||
&__background-image {
|
||||
width: 125%;
|
||||
|
@ -10186,6 +10206,9 @@ noscript {
|
|||
}
|
||||
|
||||
.notification-bar-action {
|
||||
display: inline-block;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
text-transform: uppercase;
|
||||
margin-inline-start: 10px;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@use 'variables' as *;
|
||||
|
||||
.container-alt {
|
||||
width: 700px;
|
||||
margin: 0 auto;
|
||||
|
|
39
app/javascript/styles/mastodon/css_variables.scss
Normal file
39
app/javascript/styles/mastodon/css_variables.scss
Normal file
|
@ -0,0 +1,39 @@
|
|||
@use 'sass:color';
|
||||
@use 'functions' as *;
|
||||
@use 'variables' as *;
|
||||
|
||||
:root {
|
||||
--dropdown-border-color: #{lighten($ui-base-color, 4%)};
|
||||
--dropdown-background-color: #{rgba(darken($ui-base-color, 8%), 0.9)};
|
||||
--dropdown-shadow: 0 20px 25px -5px #{rgba($base-shadow-color, 0.25)},
|
||||
0 8px 10px -6px #{rgba($base-shadow-color, 0.25)};
|
||||
--modal-background-color: #{rgba(darken($ui-base-color, 8%), 0.7)};
|
||||
--modal-background-variant-color: #{rgba($ui-base-color, 0.7)};
|
||||
--modal-border-color: #{lighten($ui-base-color, 4%)};
|
||||
--background-border-color: #{lighten($ui-base-color, 4%)};
|
||||
--background-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
|
||||
--background-color: #{darken($ui-base-color, 8%)};
|
||||
--background-color-tint: #{rgba(darken($ui-base-color, 8%), 0.9)};
|
||||
--surface-background-color: #{darken($ui-base-color, 4%)};
|
||||
--surface-variant-background-color: #{$ui-base-color};
|
||||
--surface-variant-active-background-color: #{lighten($ui-base-color, 4%)};
|
||||
--on-surface-color: #{color.adjust($ui-base-color, $alpha: -0.5)};
|
||||
--avatar-border-radius: 8px;
|
||||
--media-outline-color: #{rgba(#fcf8ff, 0.15)};
|
||||
--overlay-icon-shadow: drop-shadow(0 0 8px #{rgba($base-shadow-color, 0.25)});
|
||||
--error-background-color: #{darken($error-red, 16%)};
|
||||
--error-active-background-color: #{darken($error-red, 12%)};
|
||||
--on-error-color: #fff;
|
||||
--rich-text-container-color: rgba(87, 24, 60, 100%);
|
||||
--rich-text-text-color: rgba(255, 175, 212, 100%);
|
||||
--rich-text-decorations-color: rgba(128, 58, 95, 100%);
|
||||
--input-placeholder-color: #{$dark-text-color};
|
||||
--input-background-color: var(--surface-variant-background-color);
|
||||
--on-input-color: #{$secondary-text-color};
|
||||
--content-font-size: 15px;
|
||||
--content-emoji-size: 20px;
|
||||
--content-line-height: 22px;
|
||||
--detail-content-font-size: 19px;
|
||||
--detail-content-emoji-size: 24px;
|
||||
--detail-content-line-height: 24px;
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
@use 'functions' as *;
|
||||
@use 'variables' as *;
|
||||
|
||||
.dashboard__counters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
@use 'variables' as *;
|
||||
@use 'functions' as *;
|
||||
|
||||
.emoji-mart {
|
||||
font-size: 13px;
|
||||
display: inline-block;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
$no-columns-breakpoint: 600px;
|
||||
@use 'variables' as *;
|
||||
@use 'functions' as *;
|
||||
|
||||
code {
|
||||
font-family: $font-monospace, monospace;
|
||||
|
@ -365,6 +366,22 @@ code {
|
|||
}
|
||||
}
|
||||
|
||||
.input.date_of_birth .label_input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
box-sizing: content-box;
|
||||
width: 32px;
|
||||
flex: 0;
|
||||
|
||||
&:last-child {
|
||||
width: 64px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input.select.select--languages {
|
||||
min-width: 32ch;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
@use 'variables' as *;
|
||||
@use 'functions' as *;
|
||||
|
||||
.modal-layout {
|
||||
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>')
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
@use 'variables' as *;
|
||||
@use 'functions' as *;
|
||||
|
||||
.poll {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@use 'variables' as *;
|
||||
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
@use 'functions' as *;
|
||||
@use 'variables' as *;
|
||||
|
||||
body.rtl {
|
||||
direction: rtl;
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue