Compare commits
92 commits
b8e117e713
...
058ddd61e2
Author | SHA1 | Date | |
---|---|---|---|
058ddd61e2 | |||
|
202077517c | ||
|
0226bbe516 | ||
|
cc3cf9c465 | ||
|
a269ff9253 | ||
|
207c073bf8 | ||
|
1b6a82b799 | ||
|
f3c4874522 | ||
|
4aa600387e | ||
|
8cdc148167 | ||
|
17c57c46e7 | ||
|
a496aeabcb | ||
|
5f782f9629 | ||
|
c35ea59ee6 | ||
|
24ef8255b3 | ||
|
ab763c493f | ||
|
3d46f47817 | ||
|
f2a92c2d22 | ||
|
7d53ca56d2 | ||
|
a27f7f4e56 | ||
|
2babfafaff | ||
|
cdcd834f3c | ||
|
9769ffdcc2 | ||
|
cee71b9892 | ||
|
a3215c0f88 | ||
|
9e12fa254e | ||
|
e6f5b36a12 | ||
|
e09f9f885e | ||
|
0c3c06f7cc | ||
|
4ffaced8bc | ||
|
c4b09d684e | ||
|
da07adfe6c | ||
|
e0c27a5047 | ||
|
3929e3c6d2 | ||
|
5260233b81 | ||
|
5b995143f1 | ||
|
9ea710e543 | ||
|
592a7af27f | ||
|
d0ab94c4d2 | ||
|
a021dee642 | ||
|
2caa3f365d | ||
|
1d03570080 | ||
|
a0ea2fa3b0 | ||
|
9d9901cc5b | ||
|
e6969cf4e4 | ||
|
1f13b87567 | ||
|
c6a0768fe5 | ||
|
82161d8ae5 | ||
|
afa2e257e4 | ||
|
10143d053a | ||
|
b716248fc5 | ||
|
7335a43b6d | ||
|
0a433d08fb | ||
|
4f81ad2494 | ||
|
b530fc5267 | ||
|
c88ba523ee | ||
|
a9d0b48b65 | ||
|
fd7fc7bdc3 | ||
|
1fed11cfa7 | ||
|
ebf09328d4 | ||
|
6b6a80b407 | ||
|
be77a1098b | ||
|
cc4865193a | ||
|
60182db0ca | ||
|
7efe0bde9d | ||
|
09017dd8f0 | ||
|
5acec087ca | ||
|
bc435c63bd | ||
|
850478dc14 | ||
|
d58faa2018 | ||
|
f85694acfd | ||
|
b4b639ee4a | ||
|
e820cc30b8 | ||
|
5b1ae15a36 | ||
|
8fd3e37747 | ||
|
bd8cd0c6e7 | ||
|
f9712fad1b | ||
|
ba9fd1c32e | ||
|
b265a654d7 | ||
|
eb23d9f0f6 | ||
|
ec4c49082e | ||
|
7d91723f05 | ||
|
4d5c91e99a | ||
|
4678473e54 | ||
|
559958f8c5 | ||
|
e1b5f3fc6f | ||
|
fe04291af4 | ||
|
ef2bc8ea26 | ||
|
9ba81eae3e | ||
|
585e369e0b | ||
|
fab29ebbe8 | ||
|
1fcffa573c |
609 changed files with 5397 additions and 4489 deletions
|
@ -1,6 +1,7 @@
|
||||||
[production]
|
[production]
|
||||||
defaults
|
defaults
|
||||||
> 0.2%
|
> 0.2%
|
||||||
|
firefox >= 78
|
||||||
ios >= 15.6
|
ios >= 15.6
|
||||||
not dead
|
not dead
|
||||||
not OperaMini all
|
not OperaMini all
|
||||||
|
|
|
@ -316,7 +316,7 @@ module.exports = defineConfig({
|
||||||
],
|
],
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: true,
|
projectService: true,
|
||||||
tsconfigRootDir: __dirname,
|
tsconfigRootDir: __dirname,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
2
.github/codecov.yml
vendored
2
.github/codecov.yml
vendored
|
@ -9,3 +9,5 @@ coverage:
|
||||||
default:
|
default:
|
||||||
# GitHub status check is not blocking
|
# GitHub status check is not blocking
|
||||||
informational: true
|
informational: true
|
||||||
|
github_checks:
|
||||||
|
annotations: false
|
||||||
|
|
1
.github/renovate.json5
vendored
1
.github/renovate.json5
vendored
|
@ -7,6 +7,7 @@
|
||||||
':prConcurrentLimitNone', // Remove limit for open PRs at any time.
|
':prConcurrentLimitNone', // Remove limit for open PRs at any time.
|
||||||
':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
|
':prHourlyLimit2', // Rate limit PR creation to a maximum of two per hour.
|
||||||
],
|
],
|
||||||
|
rebaseWhen: 'conflicted',
|
||||||
minimumReleaseAge: '3', // Wait 3 days after the package has been published before upgrading it
|
minimumReleaseAge: '3', // Wait 3 days after the package has been published before upgrading it
|
||||||
// packageRules order is important, they are applied from top to bottom and are merged,
|
// packageRules order is important, they are applied from top to bottom and are merged,
|
||||||
// meaning the most important ones must be at the bottom, for example grouping rules
|
// meaning the most important ones must be at the bottom, for example grouping rules
|
||||||
|
|
2
.github/workflows/crowdin-download.yml
vendored
2
.github/workflows/crowdin-download.yml
vendored
|
@ -52,7 +52,7 @@ jobs:
|
||||||
|
|
||||||
# Create or update the pull request
|
# Create or update the pull request
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v6.0.5
|
uses: peter-evans/create-pull-request@v7.0.1
|
||||||
with:
|
with:
|
||||||
commit-message: 'New Crowdin translations'
|
commit-message: 'New Crowdin translations'
|
||||||
title: 'New Crowdin Translations (automated)'
|
title: 'New Crowdin Translations (automated)'
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# This configuration was generated by
|
# This configuration was generated by
|
||||||
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
|
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
|
||||||
# using RuboCop version 1.65.0.
|
# using RuboCop version 1.66.1.
|
||||||
# The point is for the user to remove these configuration records
|
# The point is for the user to remove these configuration records
|
||||||
# one by one as the offenses are removed from the code base.
|
# one by one as the offenses are removed from the code base.
|
||||||
# Note that changes in the inspected code, or installation of new
|
# Note that changes in the inspected code, or installation of new
|
||||||
|
@ -35,7 +35,6 @@ Rails/OutputSafety:
|
||||||
# Configuration parameters: AllowedVars.
|
# Configuration parameters: AllowedVars.
|
||||||
Style/FetchEnvVar:
|
Style/FetchEnvVar:
|
||||||
Exclude:
|
Exclude:
|
||||||
- 'app/lib/redis_configuration.rb'
|
|
||||||
- 'app/lib/translation_service.rb'
|
- 'app/lib/translation_service.rb'
|
||||||
- 'config/environments/production.rb'
|
- 'config/environments/production.rb'
|
||||||
- 'config/initializers/2_limited_federation_mode.rb'
|
- 'config/initializers/2_limited_federation_mode.rb'
|
||||||
|
@ -44,7 +43,6 @@ Style/FetchEnvVar:
|
||||||
- 'config/initializers/devise.rb'
|
- 'config/initializers/devise.rb'
|
||||||
- 'config/initializers/paperclip.rb'
|
- 'config/initializers/paperclip.rb'
|
||||||
- 'config/initializers/vapid.rb'
|
- 'config/initializers/vapid.rb'
|
||||||
- 'lib/mastodon/redis_config.rb'
|
|
||||||
- 'lib/tasks/repo.rake'
|
- 'lib/tasks/repo.rake'
|
||||||
|
|
||||||
# This cop supports safe autocorrection (--autocorrect).
|
# This cop supports safe autocorrection (--autocorrect).
|
||||||
|
@ -93,7 +91,6 @@ Style/OptionalBooleanParameter:
|
||||||
- 'app/services/fetch_resource_service.rb'
|
- 'app/services/fetch_resource_service.rb'
|
||||||
- 'app/workers/domain_block_worker.rb'
|
- 'app/workers/domain_block_worker.rb'
|
||||||
- 'app/workers/unfollow_follow_worker.rb'
|
- 'app/workers/unfollow_follow_worker.rb'
|
||||||
- 'lib/mastodon/redis_config.rb'
|
|
||||||
|
|
||||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||||
# Configuration parameters: EnforcedStyle.
|
# Configuration parameters: EnforcedStyle.
|
||||||
|
|
|
@ -12,7 +12,7 @@ ARG BUILDPLATFORM=${BUILDPLATFORM}
|
||||||
|
|
||||||
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"]
|
# Ruby image to use for base image, change with [--build-arg RUBY_VERSION="3.3.x"]
|
||||||
# renovate: datasource=docker depName=docker.io/ruby
|
# renovate: datasource=docker depName=docker.io/ruby
|
||||||
ARG RUBY_VERSION="3.3.4"
|
ARG RUBY_VERSION="3.3.5"
|
||||||
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
# # Node version to use in base image, change with [--build-arg NODE_MAJOR_VERSION="20"]
|
||||||
# renovate: datasource=node-version depName=node
|
# renovate: datasource=node-version depName=node
|
||||||
ARG NODE_MAJOR_VERSION="20"
|
ARG NODE_MAJOR_VERSION="20"
|
||||||
|
|
141
Gemfile.lock
141
Gemfile.lock
|
@ -10,35 +10,35 @@ GIT
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (7.1.3.4)
|
actioncable (7.1.4)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 7.1.4)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 7.1.4)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
websocket-driver (>= 0.6.1)
|
websocket-driver (>= 0.6.1)
|
||||||
zeitwerk (~> 2.6)
|
zeitwerk (~> 2.6)
|
||||||
actionmailbox (7.1.3.4)
|
actionmailbox (7.1.4)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 7.1.4)
|
||||||
activejob (= 7.1.3.4)
|
activejob (= 7.1.4)
|
||||||
activerecord (= 7.1.3.4)
|
activerecord (= 7.1.4)
|
||||||
activestorage (= 7.1.3.4)
|
activestorage (= 7.1.4)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 7.1.4)
|
||||||
mail (>= 2.7.1)
|
mail (>= 2.7.1)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
actionmailer (7.1.3.4)
|
actionmailer (7.1.4)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 7.1.4)
|
||||||
actionview (= 7.1.3.4)
|
actionview (= 7.1.4)
|
||||||
activejob (= 7.1.3.4)
|
activejob (= 7.1.4)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 7.1.4)
|
||||||
mail (~> 2.5, >= 2.5.4)
|
mail (~> 2.5, >= 2.5.4)
|
||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
actionpack (7.1.3.4)
|
actionpack (7.1.4)
|
||||||
actionview (= 7.1.3.4)
|
actionview (= 7.1.4)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 7.1.4)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
racc
|
racc
|
||||||
rack (>= 2.2.4)
|
rack (>= 2.2.4)
|
||||||
|
@ -46,15 +46,15 @@ GEM
|
||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
rails-html-sanitizer (~> 1.6)
|
rails-html-sanitizer (~> 1.6)
|
||||||
actiontext (7.1.3.4)
|
actiontext (7.1.4)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 7.1.4)
|
||||||
activerecord (= 7.1.3.4)
|
activerecord (= 7.1.4)
|
||||||
activestorage (= 7.1.3.4)
|
activestorage (= 7.1.4)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 7.1.4)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.1.3.4)
|
actionview (7.1.4)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 7.1.4)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
|
@ -64,22 +64,22 @@ GEM
|
||||||
activemodel (>= 4.1)
|
activemodel (>= 4.1)
|
||||||
case_transform (>= 0.2)
|
case_transform (>= 0.2)
|
||||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||||
activejob (7.1.3.4)
|
activejob (7.1.4)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 7.1.4)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.1.3.4)
|
activemodel (7.1.4)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 7.1.4)
|
||||||
activerecord (7.1.3.4)
|
activerecord (7.1.4)
|
||||||
activemodel (= 7.1.3.4)
|
activemodel (= 7.1.4)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 7.1.4)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (7.1.3.4)
|
activestorage (7.1.4)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 7.1.4)
|
||||||
activejob (= 7.1.3.4)
|
activejob (= 7.1.4)
|
||||||
activerecord (= 7.1.3.4)
|
activerecord (= 7.1.4)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 7.1.4)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (7.1.3.4)
|
activesupport (7.1.4)
|
||||||
base64
|
base64
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
|
@ -100,17 +100,17 @@ GEM
|
||||||
attr_required (1.0.2)
|
attr_required (1.0.2)
|
||||||
awrence (1.2.1)
|
awrence (1.2.1)
|
||||||
aws-eventstream (1.3.0)
|
aws-eventstream (1.3.0)
|
||||||
aws-partitions (1.970.0)
|
aws-partitions (1.974.0)
|
||||||
aws-sdk-core (3.203.0)
|
aws-sdk-core (3.205.0)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.651.0)
|
aws-partitions (~> 1, >= 1.651.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
aws-sdk-kms (1.89.0)
|
aws-sdk-kms (1.91.0)
|
||||||
aws-sdk-core (~> 3, >= 3.203.0)
|
aws-sdk-core (~> 3, >= 3.205.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.160.0)
|
aws-sdk-s3 (1.162.0)
|
||||||
aws-sdk-core (~> 3, >= 3.203.0)
|
aws-sdk-core (~> 3, >= 3.205.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.9.1)
|
aws-sigv4 (1.9.1)
|
||||||
|
@ -458,7 +458,7 @@ GEM
|
||||||
nokogiri (1.16.7)
|
nokogiri (1.16.7)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.5)
|
oj (3.16.6)
|
||||||
bigdecimal (>= 3.0)
|
bigdecimal (>= 3.0)
|
||||||
ostruct (>= 0.2)
|
ostruct (>= 0.2)
|
||||||
omniauth (2.1.2)
|
omniauth (2.1.2)
|
||||||
|
@ -584,13 +584,13 @@ GEM
|
||||||
ostruct (0.6.0)
|
ostruct (0.6.0)
|
||||||
ox (2.14.18)
|
ox (2.14.18)
|
||||||
parallel (1.26.3)
|
parallel (1.26.3)
|
||||||
parser (3.3.4.2)
|
parser (3.3.5.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
parslet (2.0.0)
|
parslet (2.0.0)
|
||||||
pastel (0.8.0)
|
pastel (0.8.0)
|
||||||
tty-color (~> 0.5)
|
tty-color (~> 0.5)
|
||||||
pg (1.5.7)
|
pg (1.5.8)
|
||||||
pghero (3.6.0)
|
pghero (3.6.0)
|
||||||
activerecord (>= 6.1)
|
activerecord (>= 6.1)
|
||||||
premailer (1.27.0)
|
premailer (1.27.0)
|
||||||
|
@ -601,7 +601,7 @@ GEM
|
||||||
actionmailer (>= 3)
|
actionmailer (>= 3)
|
||||||
net-smtp
|
net-smtp
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
propshaft (0.9.1)
|
propshaft (1.0.0)
|
||||||
actionpack (>= 7.0.0)
|
actionpack (>= 7.0.0)
|
||||||
activesupport (>= 7.0.0)
|
activesupport (>= 7.0.0)
|
||||||
rack
|
rack
|
||||||
|
@ -638,20 +638,20 @@ GEM
|
||||||
rackup (1.0.0)
|
rackup (1.0.0)
|
||||||
rack (< 3)
|
rack (< 3)
|
||||||
webrick
|
webrick
|
||||||
rails (7.1.3.4)
|
rails (7.1.4)
|
||||||
actioncable (= 7.1.3.4)
|
actioncable (= 7.1.4)
|
||||||
actionmailbox (= 7.1.3.4)
|
actionmailbox (= 7.1.4)
|
||||||
actionmailer (= 7.1.3.4)
|
actionmailer (= 7.1.4)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 7.1.4)
|
||||||
actiontext (= 7.1.3.4)
|
actiontext (= 7.1.4)
|
||||||
actionview (= 7.1.3.4)
|
actionview (= 7.1.4)
|
||||||
activejob (= 7.1.3.4)
|
activejob (= 7.1.4)
|
||||||
activemodel (= 7.1.3.4)
|
activemodel (= 7.1.4)
|
||||||
activerecord (= 7.1.3.4)
|
activerecord (= 7.1.4)
|
||||||
activestorage (= 7.1.3.4)
|
activestorage (= 7.1.4)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 7.1.4)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.1.3.4)
|
railties (= 7.1.4)
|
||||||
rails-controller-testing (1.0.5)
|
rails-controller-testing (1.0.5)
|
||||||
actionpack (>= 5.0.1.rc1)
|
actionpack (>= 5.0.1.rc1)
|
||||||
actionview (>= 5.0.1.rc1)
|
actionview (>= 5.0.1.rc1)
|
||||||
|
@ -666,9 +666,9 @@ GEM
|
||||||
rails-i18n (7.0.9)
|
rails-i18n (7.0.9)
|
||||||
i18n (>= 0.7, < 2)
|
i18n (>= 0.7, < 2)
|
||||||
railties (>= 6.0.0, < 8)
|
railties (>= 6.0.0, < 8)
|
||||||
railties (7.1.3.4)
|
railties (7.1.4)
|
||||||
actionpack (= 7.1.3.4)
|
actionpack (= 7.1.4)
|
||||||
activesupport (= 7.1.3.4)
|
activesupport (= 7.1.4)
|
||||||
irb
|
irb
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
|
@ -691,15 +691,14 @@ GEM
|
||||||
redlock (1.3.2)
|
redlock (1.3.2)
|
||||||
redis (>= 3.0.0, < 6.0)
|
redis (>= 3.0.0, < 6.0)
|
||||||
regexp_parser (2.9.2)
|
regexp_parser (2.9.2)
|
||||||
reline (0.5.9)
|
reline (0.5.10)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
request_store (1.6.0)
|
request_store (1.6.0)
|
||||||
rack (>= 1.4)
|
rack (>= 1.4)
|
||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.3.6)
|
rexml (3.3.7)
|
||||||
strscan
|
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.3.0)
|
rouge (4.3.0)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
|
@ -735,18 +734,17 @@ GEM
|
||||||
rspec-mocks (~> 3.0)
|
rspec-mocks (~> 3.0)
|
||||||
sidekiq (>= 5, < 8)
|
sidekiq (>= 5, < 8)
|
||||||
rspec-support (3.13.1)
|
rspec-support (3.13.1)
|
||||||
rubocop (1.65.1)
|
rubocop (1.66.1)
|
||||||
json (~> 2.3)
|
json (~> 2.3)
|
||||||
language_server-protocol (>= 3.17.0)
|
language_server-protocol (>= 3.17.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.3.0.2)
|
parser (>= 3.3.0.2)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 2.4, < 3.0)
|
regexp_parser (>= 2.4, < 3.0)
|
||||||
rexml (>= 3.2.5, < 4.0)
|
rubocop-ast (>= 1.32.2, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 2.4.0, < 3.0)
|
unicode-display_width (>= 2.4.0, < 3.0)
|
||||||
rubocop-ast (1.32.1)
|
rubocop-ast (1.32.3)
|
||||||
parser (>= 3.3.1.0)
|
parser (>= 3.3.1.0)
|
||||||
rubocop-capybara (2.21.0)
|
rubocop-capybara (2.21.0)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
|
@ -826,7 +824,6 @@ GEM
|
||||||
stringio (3.1.1)
|
stringio (3.1.1)
|
||||||
strong_migrations (2.0.0)
|
strong_migrations (2.0.0)
|
||||||
activerecord (>= 6.1)
|
activerecord (>= 6.1)
|
||||||
strscan (3.1.0)
|
|
||||||
swd (1.3.0)
|
swd (1.3.0)
|
||||||
activesupport (>= 3)
|
activesupport (>= 3)
|
||||||
attr_required (>= 0.0.5)
|
attr_required (>= 0.0.5)
|
||||||
|
@ -860,7 +857,7 @@ GEM
|
||||||
unf (~> 0.1.0)
|
unf (~> 0.1.0)
|
||||||
tzinfo (2.0.6)
|
tzinfo (2.0.6)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
tzinfo-data (1.2024.1)
|
tzinfo-data (1.2024.2)
|
||||||
tzinfo (>= 1.0.0)
|
tzinfo (>= 1.0.0)
|
||||||
unf (0.1.4)
|
unf (0.1.4)
|
||||||
unf_ext
|
unf_ext
|
||||||
|
|
|
@ -13,7 +13,7 @@ module Admin
|
||||||
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg')
|
redirect_to admin_account_path(@account_moderation_note.target_account_id), notice: I18n.t('admin.account_moderation_notes.created_msg')
|
||||||
else
|
else
|
||||||
@account = @account_moderation_note.target_account
|
@account = @account_moderation_note.target_account
|
||||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
@moderation_notes = @account.targeted_moderation_notes.chronological.includes(:account)
|
||||||
@warnings = @account.strikes.custom.latest
|
@warnings = @account.strikes.custom.latest
|
||||||
|
|
||||||
render 'admin/accounts/show'
|
render 'admin/accounts/show'
|
||||||
|
|
|
@ -33,7 +33,7 @@ module Admin
|
||||||
|
|
||||||
@deletion_request = @account.deletion_request
|
@deletion_request = @account.deletion_request
|
||||||
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
@account_moderation_note = current_account.account_moderation_notes.new(target_account: @account)
|
||||||
@moderation_notes = @account.targeted_moderation_notes.latest
|
@moderation_notes = @account.targeted_moderation_notes.chronological.includes(:account)
|
||||||
@warnings = @account.strikes.includes(:target_account, :account, :appeal).latest
|
@warnings = @account.strikes.includes(:target_account, :account, :appeal).latest
|
||||||
@domain_block = DomainBlock.rule_for(@account.domain)
|
@domain_block = DomainBlock.rule_for(@account.domain)
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,17 +7,12 @@ module Admin
|
||||||
|
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
after_action :verify_authorized
|
after_action :verify_authorized
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.cache_control.replace(private: true, no_store: true)
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,12 +7,12 @@ module Admin
|
||||||
def index
|
def index
|
||||||
authorize :dashboard, :index?
|
authorize :dashboard, :index?
|
||||||
|
|
||||||
|
@pending_appeals_count = Appeal.pending.async_count
|
||||||
|
@pending_reports_count = Report.unresolved.async_count
|
||||||
|
@pending_tags_count = Tag.pending_review.async_count
|
||||||
|
@pending_users_count = User.pending.async_count
|
||||||
@system_checks = Admin::SystemCheck.perform(current_user)
|
@system_checks = Admin::SystemCheck.perform(current_user)
|
||||||
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
|
@time_period = (29.days.ago.to_date...Time.now.utc.to_date)
|
||||||
@pending_users_count = User.pending.count
|
|
||||||
@pending_reports_count = Report.unresolved.count
|
|
||||||
@pending_tags_count = Tag.pending_review.count
|
|
||||||
@pending_appeals_count = Appeal.pending.count
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,7 +21,7 @@ module Admin
|
||||||
|
|
||||||
redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg')
|
redirect_to after_create_redirect_path, notice: I18n.t('admin.report_notes.created_msg')
|
||||||
else
|
else
|
||||||
@report_notes = @report.notes.includes(:account).order(id: :desc)
|
@report_notes = @report.notes.chronological.includes(:account)
|
||||||
@action_logs = @report.history.includes(:target)
|
@action_logs = @report.history.includes(:target)
|
||||||
@form = Admin::StatusBatchAction.new
|
@form = Admin::StatusBatchAction.new
|
||||||
@statuses = @report.statuses.with_includes
|
@statuses = @report.statuses.with_includes
|
||||||
|
|
|
@ -13,7 +13,7 @@ module Admin
|
||||||
authorize @report, :show?
|
authorize @report, :show?
|
||||||
|
|
||||||
@report_note = @report.notes.new
|
@report_note = @report.notes.new
|
||||||
@report_notes = @report.notes.includes(:account).order(id: :desc)
|
@report_notes = @report.notes.chronological.includes(:account)
|
||||||
@action_logs = @report.history.includes(:target)
|
@action_logs = @report.history.includes(:target)
|
||||||
@form = Admin::StatusBatchAction.new
|
@form = Admin::StatusBatchAction.new
|
||||||
@statuses = @report.statuses.with_includes
|
@statuses = @report.statuses.with_includes
|
||||||
|
|
|
@ -77,6 +77,8 @@ class Api::V2Alpha::NotificationsController < Api::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_grouped_notifications
|
def load_grouped_notifications
|
||||||
|
return [] if @notifications.empty?
|
||||||
|
|
||||||
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_grouped_notifications') do
|
MastodonOTELTracer.in_span('Api::V2Alpha::NotificationsController#load_grouped_notifications') do
|
||||||
NotificationGroup.from_notifications(@notifications, pagination_range: (@notifications.last.id)..(@notifications.first.id), grouped_types: params[:grouped_types])
|
NotificationGroup.from_notifications(@notifications, pagination_range: (@notifications.last.id)..(@notifications.first.id), grouped_types: params[:grouped_types])
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
before_action :configure_sign_up_params, only: [:create]
|
before_action :configure_sign_up_params, only: [:create]
|
||||||
before_action :set_sessions, only: [:edit, :update]
|
before_action :set_sessions, only: [:edit, :update]
|
||||||
before_action :set_strikes, only: [:edit, :update]
|
before_action :set_strikes, only: [:edit, :update]
|
||||||
before_action :set_body_classes, only: [:new, :create, :edit, :update]
|
|
||||||
before_action :require_not_suspended!, only: [:update]
|
before_action :require_not_suspended!, only: [:update]
|
||||||
before_action :set_cache_headers, only: [:edit, :update]
|
before_action :set_cache_headers, only: [:edit, :update]
|
||||||
before_action :set_rules, only: :new
|
before_action :set_rules, only: :new
|
||||||
|
@ -104,10 +103,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'admin' if %w(edit update).include?(action_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_invite
|
def set_invite
|
||||||
@invite = begin
|
@invite = begin
|
||||||
invite = Invite.find_by(code: invite_code) if invite_code.present?
|
invite = Invite.find_by(code: invite_code) if invite_code.present?
|
||||||
|
|
|
@ -20,7 +20,7 @@ module AccountControllerConcern
|
||||||
webfinger_account_link,
|
webfinger_account_link,
|
||||||
actor_url_link,
|
actor_url_link,
|
||||||
]
|
]
|
||||||
)
|
).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def webfinger_account_link
|
def webfinger_account_link
|
||||||
|
|
|
@ -19,7 +19,7 @@ module Api::Pagination
|
||||||
links = []
|
links = []
|
||||||
links << [next_path, [%w(rel next)]] if next_path
|
links << [next_path, [%w(rel next)]] if next_path
|
||||||
links << [prev_path, [%w(rel prev)]] if prev_path
|
links << [prev_path, [%w(rel prev)]] if prev_path
|
||||||
response.headers['Link'] = LinkHeader.new(links) unless links.empty?
|
response.headers['Link'] = LinkHeader.new(links).to_s unless links.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
def require_valid_pagination_options!
|
def require_valid_pagination_options!
|
||||||
|
|
|
@ -8,6 +8,16 @@ module WebAppControllerConcern
|
||||||
|
|
||||||
before_action :redirect_unauthenticated_to_permalinks!
|
before_action :redirect_unauthenticated_to_permalinks!
|
||||||
before_action :set_app_body_class
|
before_action :set_app_body_class
|
||||||
|
|
||||||
|
content_security_policy do |p|
|
||||||
|
policy = ContentSecurityPolicy.new
|
||||||
|
|
||||||
|
if policy.sso_host.present?
|
||||||
|
p.form_action policy.sso_host
|
||||||
|
else
|
||||||
|
p.form_action :none
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def skip_csrf_meta_tags?
|
def skip_csrf_meta_tags?
|
||||||
|
|
|
@ -7,16 +7,11 @@ class Disputes::BaseController < ApplicationController
|
||||||
|
|
||||||
skip_before_action :require_functional!
|
skip_before_action :require_functional!
|
||||||
|
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.cache_control.replace(private: true, no_store: true)
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,6 @@ class Filters::StatusesController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_filter
|
before_action :set_filter
|
||||||
before_action :set_status_filters
|
before_action :set_status_filters
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
PER_PAGE = 20
|
PER_PAGE = 20
|
||||||
|
@ -42,10 +41,6 @@ class Filters::StatusesController < ApplicationController
|
||||||
'remove' if params[:remove]
|
'remove' if params[:remove]
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.cache_control.replace(private: true, no_store: true)
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,6 @@ class FiltersController < ApplicationController
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_filter, only: [:edit, :update, :destroy]
|
before_action :set_filter, only: [:edit, :update, :destroy]
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -52,10 +51,6 @@ class FiltersController < ApplicationController
|
||||||
params.require(:custom_filter).permit(:title, :expires_in, :filter_action, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
|
params.require(:custom_filter).permit(:title, :expires_in, :filter_action, context: [], keywords_attributes: [:id, :keyword, :whole_word, :_destroy])
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.cache_control.replace(private: true, no_store: true)
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,6 @@ class InvitesController < ApplicationController
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def index
|
def index
|
||||||
|
@ -47,10 +46,6 @@ class InvitesController < ApplicationController
|
||||||
params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment)
|
params.require(:invite).permit(:max_uses, :expires_in, :autofollow, :comment)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.cache_control.replace(private: true, no_store: true)
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,9 +19,7 @@ class MediaController < ApplicationController
|
||||||
redirect_to @media_attachment.file.url(:original)
|
redirect_to @media_attachment.file.url(:original)
|
||||||
end
|
end
|
||||||
|
|
||||||
def player
|
def player; end
|
||||||
@body_classes = 'player'
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
||||||
before_action :store_current_location
|
before_action :store_current_location
|
||||||
before_action :authenticate_resource_owner!
|
before_action :authenticate_resource_owner!
|
||||||
before_action :require_not_suspended!, only: :destroy
|
before_action :require_not_suspended!, only: :destroy
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
|
before_action :set_last_used_at_by_app, only: :index, unless: -> { request.format == :json }
|
||||||
|
@ -23,10 +22,6 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def store_current_location
|
def store_current_location
|
||||||
store_location_for(:user, request.url)
|
store_location_for(:user, request.url)
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,6 @@ class Redirect::BaseController < ApplicationController
|
||||||
vary_by 'Accept-Language'
|
vary_by 'Accept-Language'
|
||||||
|
|
||||||
before_action :set_resource
|
before_action :set_resource
|
||||||
before_action :set_app_body_class
|
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@redirect_path = ActivityPub::TagManager.instance.url_for(@resource)
|
@redirect_path = ActivityPub::TagManager.instance.url_for(@resource)
|
||||||
|
@ -14,10 +13,6 @@ class Redirect::BaseController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_app_body_class
|
|
||||||
@body_classes = 'app-body'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_resource
|
def set_resource
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,7 +6,6 @@ class RelationshipsController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_accounts, only: :show
|
before_action :set_accounts, only: :show
|
||||||
before_action :set_relationships, only: :show
|
before_action :set_relationships, only: :show
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship?
|
helper_method :following_relationship?, :followed_by_relationship?, :mutual_relationship?
|
||||||
|
@ -68,10 +67,6 @@ class RelationshipsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.cache_control.replace(private: true, no_store: true)
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,15 +4,10 @@ class Settings::BaseController < ApplicationController
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.cache_control.replace(private: true, no_store: true)
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,14 +2,30 @@
|
||||||
|
|
||||||
class Settings::VerificationsController < Settings::BaseController
|
class Settings::VerificationsController < Settings::BaseController
|
||||||
before_action :set_account
|
before_action :set_account
|
||||||
|
before_action :set_verified_links
|
||||||
|
|
||||||
def show
|
def show; end
|
||||||
@verified_links = @account.fields.select(&:verified?)
|
|
||||||
|
def update
|
||||||
|
if UpdateAccountService.new.call(@account, account_params)
|
||||||
|
ActivityPub::UpdateDistributionWorker.perform_async(@account.id)
|
||||||
|
redirect_to settings_verification_path, notice: I18n.t('generic.changes_saved_msg')
|
||||||
|
else
|
||||||
|
render :show
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def account_params
|
||||||
|
params.require(:account).permit(:attribution_domains_as_text)
|
||||||
|
end
|
||||||
|
|
||||||
def set_account
|
def set_account
|
||||||
@account = current_account
|
@account = current_account
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_verified_links
|
||||||
|
@verified_links = @account.fields.select(&:verified?)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,6 @@ class SeveredRelationshipsController < ApplicationController
|
||||||
layout 'admin'
|
layout 'admin'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
before_action :set_event, only: [:following, :followers]
|
before_action :set_event, only: [:following, :followers]
|
||||||
|
@ -51,10 +50,6 @@ class SeveredRelationshipsController < ApplicationController
|
||||||
account.local? ? account.local_username_and_domain : account.acct
|
account.local? ? account.local_username_and_domain : account.acct
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.cache_control.replace(private: true, no_store: true)
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,13 +4,6 @@ class SharesController < ApplicationController
|
||||||
layout 'modal'
|
layout 'modal'
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_body_classes
|
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'modal-layout compose-standalone'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,6 @@ class StatusesCleanupController < ApplicationController
|
||||||
|
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
before_action :set_policy
|
before_action :set_policy
|
||||||
before_action :set_body_classes
|
|
||||||
before_action :set_cache_headers
|
before_action :set_cache_headers
|
||||||
|
|
||||||
def show; end
|
def show; end
|
||||||
|
@ -34,10 +33,6 @@ class StatusesCleanupController < ApplicationController
|
||||||
params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :min_favs, :min_reblogs)
|
params.require(:account_statuses_cleanup_policy).permit(:enabled, :min_status_age, :keep_direct, :keep_pinned, :keep_polls, :keep_media, :keep_self_fav, :keep_self_bookmark, :min_favs, :min_reblogs)
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'admin'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_cache_headers
|
def set_cache_headers
|
||||||
response.cache_control.replace(private: true, no_store: true)
|
response.cache_control.replace(private: true, no_store: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,6 @@ class StatusesController < ApplicationController
|
||||||
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
before_action :require_account_signature!, only: [:show, :activity], if: -> { request.format == :json && authorized_fetch_mode? }
|
||||||
before_action :set_status
|
before_action :set_status
|
||||||
before_action :redirect_to_original, only: :show
|
before_action :redirect_to_original, only: :show
|
||||||
before_action :set_body_classes, only: :embed
|
|
||||||
|
|
||||||
after_action :set_link_headers
|
after_action :set_link_headers
|
||||||
|
|
||||||
|
@ -51,12 +50,10 @@ class StatusesController < ApplicationController
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_body_classes
|
|
||||||
@body_classes = 'with-modals'
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_link_headers
|
def set_link_headers
|
||||||
response.headers['Link'] = LinkHeader.new([[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]])
|
response.headers['Link'] = LinkHeader.new(
|
||||||
|
[[ActivityPub::TagManager.instance.uri_for(@status), [%w(rel alternate), %w(type application/activity+json)]]]
|
||||||
|
).to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_status
|
def set_status
|
||||||
|
|
|
@ -19,14 +19,6 @@ module AccountsHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def account_action_button(account)
|
|
||||||
return if account.memorial? || account.moved?
|
|
||||||
|
|
||||||
link_to ActivityPub::TagManager.instance.url_for(account), class: 'button logo-button', target: '_new' do
|
|
||||||
safe_join([logo_as_symbol, t('accounts.follow')])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def account_formatted_stat(value)
|
def account_formatted_stat(value)
|
||||||
number_to_human(value, precision: 3, strip_insignificant_zeros: true)
|
number_to_human(value, precision: 3, strip_insignificant_zeros: true)
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ module Admin::Trends::StatusesHelper
|
||||||
text = if status.local?
|
text = if status.local?
|
||||||
status.text.split("\n").first
|
status.text.split("\n").first
|
||||||
else
|
else
|
||||||
Nokogiri::HTML(status.text).css('html > body > *').first&.text
|
Nokogiri::HTML5(status.text).css('html > body > *').first&.text
|
||||||
end
|
end
|
||||||
|
|
||||||
return '' if text.blank?
|
return '' if text.blank?
|
||||||
|
|
|
@ -159,6 +159,7 @@ module ApplicationHelper
|
||||||
|
|
||||||
def body_classes
|
def body_classes
|
||||||
output = body_class_string.split
|
output = body_class_string.split
|
||||||
|
output << content_for(:body_classes)
|
||||||
output << "theme-#{current_theme.parameterize}"
|
output << "theme-#{current_theme.parameterize}"
|
||||||
output << 'system-font' if current_account&.user&.setting_system_font_ui
|
output << 'system-font' if current_account&.user&.setting_system_font_ui
|
||||||
output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion')
|
output << (current_account&.user&.setting_reduce_motion ? 'reduce-motion' : 'no-reduce-motion')
|
||||||
|
|
|
@ -41,6 +41,7 @@ module ContextHelper
|
||||||
'cipherText' => 'toot:cipherText',
|
'cipherText' => 'toot:cipherText',
|
||||||
},
|
},
|
||||||
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
||||||
|
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
def full_context
|
def full_context
|
||||||
|
|
|
@ -57,26 +57,6 @@ module MediaComponentHelper
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_card_component(status, **options)
|
|
||||||
component_params = {
|
|
||||||
sensitive: sensitive_viewer?(status, current_account),
|
|
||||||
card: serialize_status_card(status).as_json,
|
|
||||||
}.merge(**options)
|
|
||||||
|
|
||||||
react_component :card, component_params
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_poll_component(status, **options)
|
|
||||||
component_params = {
|
|
||||||
disabled: true,
|
|
||||||
poll: serialize_status_poll(status).as_json,
|
|
||||||
}.merge(**options)
|
|
||||||
|
|
||||||
react_component :poll, component_params do
|
|
||||||
render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: prefers_autoplay? }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def serialize_media_attachment(attachment)
|
def serialize_media_attachment(attachment)
|
||||||
|
@ -86,22 +66,6 @@ module MediaComponentHelper
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def serialize_status_card(status)
|
|
||||||
ActiveModelSerializers::SerializableResource.new(
|
|
||||||
status.preview_card,
|
|
||||||
serializer: REST::PreviewCardSerializer
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def serialize_status_poll(status)
|
|
||||||
ActiveModelSerializers::SerializableResource.new(
|
|
||||||
status.preloadable_poll,
|
|
||||||
serializer: REST::PollSerializer,
|
|
||||||
scope: current_user,
|
|
||||||
scope_name: :current_user
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def sensitive_viewer?(status, account)
|
def sensitive_viewer?(status, account)
|
||||||
if !account.nil? && account.id == status.account_id
|
if !account.nil? && account.id == status.account_id
|
||||||
status.sensitive
|
status.sensitive
|
||||||
|
|
|
@ -4,6 +4,13 @@ module StatusesHelper
|
||||||
EMBEDDED_CONTROLLER = 'statuses'
|
EMBEDDED_CONTROLLER = 'statuses'
|
||||||
EMBEDDED_ACTION = 'embed'
|
EMBEDDED_ACTION = 'embed'
|
||||||
|
|
||||||
|
VISIBLITY_ICONS = {
|
||||||
|
public: 'globe',
|
||||||
|
unlisted: 'lock_open',
|
||||||
|
private: 'lock',
|
||||||
|
direct: 'alternate_email',
|
||||||
|
}.freeze
|
||||||
|
|
||||||
def nothing_here(extra_classes = '')
|
def nothing_here(extra_classes = '')
|
||||||
content_tag(:div, class: "nothing-here #{extra_classes}") do
|
content_tag(:div, class: "nothing-here #{extra_classes}") do
|
||||||
t('accounts.nothing_here')
|
t('accounts.nothing_here')
|
||||||
|
@ -57,17 +64,8 @@ module StatusesHelper
|
||||||
embedded_view? ? '_blank' : nil
|
embedded_view? ? '_blank' : nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def fa_visibility_icon(status)
|
def visibility_icon(status)
|
||||||
case status.visibility
|
VISIBLITY_ICONS[status.visibility.to_sym]
|
||||||
when 'public'
|
|
||||||
material_symbol 'globe'
|
|
||||||
when 'unlisted'
|
|
||||||
material_symbol 'lock_open'
|
|
||||||
when 'private'
|
|
||||||
material_symbol 'lock'
|
|
||||||
when 'direct'
|
|
||||||
material_symbol 'alternate_email'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def embedded_view?
|
def embedded_view?
|
||||||
|
|
74
app/javascript/entrypoints/embed.tsx
Normal file
74
app/javascript/entrypoints/embed.tsx
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import './public-path';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|
||||||
|
import { afterInitialRender } from 'mastodon/../hooks/useRenderSignal';
|
||||||
|
|
||||||
|
import { start } from '../mastodon/common';
|
||||||
|
import { Status } from '../mastodon/features/standalone/status';
|
||||||
|
import { loadPolyfills } from '../mastodon/polyfills';
|
||||||
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
|
start();
|
||||||
|
|
||||||
|
function loaded() {
|
||||||
|
const mountNode = document.getElementById('mastodon-status');
|
||||||
|
|
||||||
|
if (mountNode) {
|
||||||
|
const attr = mountNode.getAttribute('data-props');
|
||||||
|
|
||||||
|
if (!attr) return;
|
||||||
|
|
||||||
|
const props = JSON.parse(attr) as { id: string; locale: string };
|
||||||
|
const root = createRoot(mountNode);
|
||||||
|
|
||||||
|
root.render(<Status {...props} />);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
ready(loaded).catch((error: unknown) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPolyfills()
|
||||||
|
.then(main)
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SetHeightMessage {
|
||||||
|
type: 'setHeight';
|
||||||
|
id: string;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSetHeightMessage(data: unknown): data is SetHeightMessage {
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
typeof data === 'object' &&
|
||||||
|
'type' in data &&
|
||||||
|
data.type === 'setHeight'
|
||||||
|
)
|
||||||
|
return true;
|
||||||
|
else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('message', (e) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
|
||||||
|
if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
|
||||||
|
|
||||||
|
const data = e.data;
|
||||||
|
|
||||||
|
// We use a timeout to allow for the React page to render before calculating the height
|
||||||
|
afterInitialRender(() => {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: 'setHeight',
|
||||||
|
id: data.id,
|
||||||
|
height: document.getElementsByTagName('html')[0]?.scrollHeight,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
|
@ -37,43 +37,6 @@ const messages = defineMessages({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
interface SetHeightMessage {
|
|
||||||
type: 'setHeight';
|
|
||||||
id: string;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSetHeightMessage(data: unknown): data is SetHeightMessage {
|
|
||||||
if (
|
|
||||||
data &&
|
|
||||||
typeof data === 'object' &&
|
|
||||||
'type' in data &&
|
|
||||||
data.type === 'setHeight'
|
|
||||||
)
|
|
||||||
return true;
|
|
||||||
else return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('message', (e) => {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- typings are not correct, it can be null in very rare cases
|
|
||||||
if (!e.data || !isSetHeightMessage(e.data) || !window.parent) return;
|
|
||||||
|
|
||||||
const data = e.data;
|
|
||||||
|
|
||||||
ready(() => {
|
|
||||||
window.parent.postMessage(
|
|
||||||
{
|
|
||||||
type: 'setHeight',
|
|
||||||
id: data.id,
|
|
||||||
height: document.getElementsByTagName('html')[0]?.scrollHeight,
|
|
||||||
},
|
|
||||||
'*',
|
|
||||||
);
|
|
||||||
}).catch((e: unknown) => {
|
|
||||||
console.error('Error in setHeightMessage postMessage', e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function loaded() {
|
function loaded() {
|
||||||
const { messages: localeData } = getLocale();
|
const { messages: localeData } = getLocale();
|
||||||
|
|
||||||
|
|
32
app/javascript/hooks/useRenderSignal.ts
Normal file
32
app/javascript/hooks/useRenderSignal.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// This hook allows a component to signal that it's done rendering in a way that
|
||||||
|
// can be used by e.g. our embed code to determine correct iframe height
|
||||||
|
|
||||||
|
let renderSignalReceived = false;
|
||||||
|
|
||||||
|
type Callback = () => void;
|
||||||
|
|
||||||
|
let onInitialRender: Callback;
|
||||||
|
|
||||||
|
export const afterInitialRender = (callback: Callback) => {
|
||||||
|
if (renderSignalReceived) {
|
||||||
|
callback();
|
||||||
|
} else {
|
||||||
|
onInitialRender = callback;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRenderSignal = () => {
|
||||||
|
return () => {
|
||||||
|
if (renderSignalReceived) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSignalReceived = true;
|
||||||
|
|
||||||
|
if (typeof onInitialRender !== 'undefined') {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
onInitialRender();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
|
@ -65,7 +65,7 @@ export const synchronouslySubmitMarkers = createAppAsyncThunk(
|
||||||
client.setRequestHeader('Content-Type', 'application/json');
|
client.setRequestHeader('Content-Type', 'application/json');
|
||||||
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
|
client.setRequestHeader('Authorization', `Bearer ${accessToken}`);
|
||||||
client.send(JSON.stringify(params));
|
client.send(JSON.stringify(params));
|
||||||
} catch (e) {
|
} catch {
|
||||||
// Do not make the BeforeUnload handler error out
|
// Do not make the BeforeUnload handler error out
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
selectSettingsNotificationsQuickFilterActive,
|
selectSettingsNotificationsQuickFilterActive,
|
||||||
selectSettingsNotificationsShows,
|
selectSettingsNotificationsShows,
|
||||||
} from 'mastodon/selectors/settings';
|
} from 'mastodon/selectors/settings';
|
||||||
import type { AppDispatch } from 'mastodon/store';
|
import type { AppDispatch, RootState } from 'mastodon/store';
|
||||||
import {
|
import {
|
||||||
createAppAsyncThunk,
|
createAppAsyncThunk,
|
||||||
createDataLoadingThunk,
|
createDataLoadingThunk,
|
||||||
|
@ -32,6 +32,14 @@ function excludeAllTypesExcept(filter: string) {
|
||||||
return allNotificationTypes.filter((item) => item !== filter);
|
return allNotificationTypes.filter((item) => item !== filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExcludedTypes(state: RootState) {
|
||||||
|
const activeFilter = selectSettingsNotificationsQuickFilterActive(state);
|
||||||
|
|
||||||
|
return activeFilter === 'all'
|
||||||
|
? selectSettingsNotificationsExcludedTypes(state)
|
||||||
|
: excludeAllTypesExcept(activeFilter);
|
||||||
|
}
|
||||||
|
|
||||||
function dispatchAssociatedRecords(
|
function dispatchAssociatedRecords(
|
||||||
dispatch: AppDispatch,
|
dispatch: AppDispatch,
|
||||||
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
|
notifications: ApiNotificationGroupJSON[] | ApiNotificationJSON[],
|
||||||
|
@ -62,17 +70,8 @@ function dispatchAssociatedRecords(
|
||||||
|
|
||||||
export const fetchNotifications = createDataLoadingThunk(
|
export const fetchNotifications = createDataLoadingThunk(
|
||||||
'notificationGroups/fetch',
|
'notificationGroups/fetch',
|
||||||
async (_params, { getState }) => {
|
async (_params, { getState }) =>
|
||||||
const activeFilter =
|
apiFetchNotifications({ exclude_types: getExcludedTypes(getState()) }),
|
||||||
selectSettingsNotificationsQuickFilterActive(getState());
|
|
||||||
|
|
||||||
return apiFetchNotifications({
|
|
||||||
exclude_types:
|
|
||||||
activeFilter === 'all'
|
|
||||||
? selectSettingsNotificationsExcludedTypes(getState())
|
|
||||||
: excludeAllTypesExcept(activeFilter),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
({ notifications, accounts, statuses }, { dispatch }) => {
|
({ notifications, accounts, statuses }, { dispatch }) => {
|
||||||
dispatch(importFetchedAccounts(accounts));
|
dispatch(importFetchedAccounts(accounts));
|
||||||
dispatch(importFetchedStatuses(statuses));
|
dispatch(importFetchedStatuses(statuses));
|
||||||
|
@ -92,9 +91,11 @@ export const fetchNotifications = createDataLoadingThunk(
|
||||||
|
|
||||||
export const fetchNotificationsGap = createDataLoadingThunk(
|
export const fetchNotificationsGap = createDataLoadingThunk(
|
||||||
'notificationGroups/fetchGap',
|
'notificationGroups/fetchGap',
|
||||||
async (params: { gap: NotificationGap }) =>
|
async (params: { gap: NotificationGap }, { getState }) =>
|
||||||
apiFetchNotifications({ max_id: params.gap.maxId }),
|
apiFetchNotifications({
|
||||||
|
max_id: params.gap.maxId,
|
||||||
|
exclude_types: getExcludedTypes(getState()),
|
||||||
|
}),
|
||||||
({ notifications, accounts, statuses }, { dispatch }) => {
|
({ notifications, accounts, statuses }, { dispatch }) => {
|
||||||
dispatch(importFetchedAccounts(accounts));
|
dispatch(importFetchedAccounts(accounts));
|
||||||
dispatch(importFetchedStatuses(statuses));
|
dispatch(importFetchedStatuses(statuses));
|
||||||
|
@ -109,6 +110,7 @@ export const pollRecentNotifications = createDataLoadingThunk(
|
||||||
async (_params, { getState }) => {
|
async (_params, { getState }) => {
|
||||||
return apiFetchNotifications({
|
return apiFetchNotifications({
|
||||||
max_id: undefined,
|
max_id: undefined,
|
||||||
|
exclude_types: getExcludedTypes(getState()),
|
||||||
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
|
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
|
||||||
since_id: usePendingItems
|
since_id: usePendingItems
|
||||||
? getState().notificationGroups.groups.find(
|
? getState().notificationGroups.groups.find(
|
||||||
|
@ -183,7 +185,6 @@ export const setNotificationsFilter = createAppAsyncThunk(
|
||||||
path: ['notifications', 'quickFilter', 'active'],
|
path: ['notifications', 'quickFilter', 'active'],
|
||||||
value: filterType,
|
value: filterType,
|
||||||
});
|
});
|
||||||
// dispatch(expandNotifications({ forceLoad: true }));
|
|
||||||
void dispatch(fetchNotifications());
|
void dispatch(fetchNotifications());
|
||||||
dispatch(saveSettings());
|
dispatch(saveSettings());
|
||||||
},
|
},
|
||||||
|
|
|
@ -49,11 +49,13 @@ export function fetchStatusRequest(id, skipLoading) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchStatus(id, forceFetch = false) {
|
export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
|
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
|
||||||
|
|
||||||
|
if (alsoFetchContext) {
|
||||||
dispatch(fetchContext(id));
|
dispatch(fetchContext(id));
|
||||||
|
}
|
||||||
|
|
||||||
if (skipLoading) {
|
if (skipLoading) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -151,7 +151,7 @@ async function refreshHomeTimelineAndNotification(dispatch, getState) {
|
||||||
// TODO: polling for merged notifications
|
// TODO: polling for merged notifications
|
||||||
try {
|
try {
|
||||||
await dispatch(pollRecentGroupNotifications());
|
await dispatch(pollRecentGroupNotifications());
|
||||||
} catch (error) {
|
} catch {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -5,7 +5,7 @@ export function start() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Rails.start();
|
Rails.start();
|
||||||
} catch (e) {
|
} catch {
|
||||||
// If called twice
|
// If called twice
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
90
app/javascript/mastodon/components/copy_paste_text.tsx
Normal file
90
app/javascript/mastodon/components/copy_paste_text.tsx
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import { useRef, useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
|
||||||
|
import { useTimeout } from 'mastodon/../hooks/useTimeout';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
|
||||||
|
export const CopyPasteText: React.FC<{ value: string }> = ({ value }) => {
|
||||||
|
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [focused, setFocused] = useState(false);
|
||||||
|
const [setAnimationTimeout] = useTimeout();
|
||||||
|
|
||||||
|
const handleInputClick = useCallback(() => {
|
||||||
|
setCopied(false);
|
||||||
|
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.focus();
|
||||||
|
inputRef.current.select();
|
||||||
|
inputRef.current.setSelectionRange(0, value.length);
|
||||||
|
}
|
||||||
|
}, [setCopied, value]);
|
||||||
|
|
||||||
|
const handleButtonClick = useCallback(
|
||||||
|
(e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
void navigator.clipboard.writeText(value);
|
||||||
|
inputRef.current?.blur();
|
||||||
|
setCopied(true);
|
||||||
|
setAnimationTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 700);
|
||||||
|
},
|
||||||
|
[setCopied, setAnimationTimeout, value],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleKeyUp = useCallback(
|
||||||
|
(e: React.KeyboardEvent) => {
|
||||||
|
if (e.key !== ' ') return;
|
||||||
|
void navigator.clipboard.writeText(value);
|
||||||
|
setCopied(true);
|
||||||
|
setAnimationTimeout(() => {
|
||||||
|
setCopied(false);
|
||||||
|
}, 700);
|
||||||
|
},
|
||||||
|
[setCopied, setAnimationTimeout, value],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFocus = useCallback(() => {
|
||||||
|
setFocused(true);
|
||||||
|
}, [setFocused]);
|
||||||
|
|
||||||
|
const handleBlur = useCallback(() => {
|
||||||
|
setFocused(false);
|
||||||
|
}, [setFocused]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames('copy-paste-text', { copied, focused })}
|
||||||
|
tabIndex={0}
|
||||||
|
role='button'
|
||||||
|
onClick={handleInputClick}
|
||||||
|
onKeyUp={handleKeyUp}
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
readOnly
|
||||||
|
value={value}
|
||||||
|
ref={inputRef}
|
||||||
|
onClick={handleInputClick}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button className='button' onClick={handleButtonClick}>
|
||||||
|
<Icon id='copy' icon={ContentCopyIcon} />{' '}
|
||||||
|
{copied ? (
|
||||||
|
<FormattedMessage id='copypaste.copied' defaultMessage='Copied' />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage
|
||||||
|
id='copypaste.copy_to_clipboard'
|
||||||
|
defaultMessage='Copy to clipboard'
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -60,8 +60,8 @@ export default class ErrorBoundary extends PureComponent {
|
||||||
try {
|
try {
|
||||||
textarea.select();
|
textarea.select();
|
||||||
document.execCommand('copy');
|
document.execCommand('copy');
|
||||||
} catch (e) {
|
} catch {
|
||||||
|
// do nothing
|
||||||
} finally {
|
} finally {
|
||||||
document.body.removeChild(textarea);
|
document.body.removeChild(textarea);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,13 @@ export const WordmarkLogo: React.FC = () => (
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const IconLogo: React.FC = () => (
|
||||||
|
<svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
|
||||||
|
<title>Mastodon</title>
|
||||||
|
<use xlinkHref='#logo-symbol-icon' />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
export const SymbolLogo: React.FC = () => (
|
export const SymbolLogo: React.FC = () => (
|
||||||
<img src={logo} alt='Mastodon' className='logo logo--icon' />
|
<img src={logo} alt='Mastodon' className='logo logo--icon' />
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
@ -10,17 +10,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
|
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react';
|
|
||||||
import { Blurhash } from 'mastodon/components/blurhash';
|
import { Blurhash } from 'mastodon/components/blurhash';
|
||||||
|
|
||||||
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
|
import { autoPlayGif, displayMedia, useBlurhash } from '../initial_state';
|
||||||
|
|
||||||
import { IconButton } from './icon_button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: '{number, plural, one {Hide image} other {Hide images}}' },
|
|
||||||
});
|
|
||||||
|
|
||||||
class Item extends PureComponent {
|
class Item extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -215,7 +208,6 @@ class MediaGallery extends PureComponent {
|
||||||
size: PropTypes.object,
|
size: PropTypes.object,
|
||||||
height: PropTypes.number.isRequired,
|
height: PropTypes.number.isRequired,
|
||||||
onOpenMedia: PropTypes.func.isRequired,
|
onOpenMedia: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
defaultWidth: PropTypes.number,
|
defaultWidth: PropTypes.number,
|
||||||
cacheWidth: PropTypes.func,
|
cacheWidth: PropTypes.func,
|
||||||
visible: PropTypes.bool,
|
visible: PropTypes.bool,
|
||||||
|
@ -291,7 +283,7 @@ class MediaGallery extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { media, lang, intl, sensitive, defaultWidth, autoplay } = this.props;
|
const { media, lang, sensitive, defaultWidth, autoplay } = this.props;
|
||||||
const { visible } = this.state;
|
const { visible } = this.state;
|
||||||
const width = this.state.width || defaultWidth;
|
const width = this.state.width || defaultWidth;
|
||||||
|
|
||||||
|
@ -323,9 +315,7 @@ class MediaGallery extends PureComponent {
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
} else if (visible) {
|
} else if (!visible) {
|
||||||
spoilerButton = <IconButton title={intl.formatMessage(messages.toggle_visible, { number: size })} icon='eye-slash' iconComponent={VisibilityOffIcon} overlay onClick={this.handleOpen} ariaHidden />;
|
|
||||||
} else {
|
|
||||||
spoilerButton = (
|
spoilerButton = (
|
||||||
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
|
<button type='button' onClick={this.handleOpen} className='spoiler-button__overlay'>
|
||||||
<span className='spoiler-button__overlay__label'>
|
<span className='spoiler-button__overlay__label'>
|
||||||
|
@ -337,16 +327,24 @@ class MediaGallery extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='media-gallery' style={style} ref={this.handleRef}>
|
<div className={`media-gallery media-gallery--layout-${size}`} style={style} ref={this.handleRef}>
|
||||||
<div className={classNames('spoiler-button', { 'spoiler-button--minified': visible && !uncached, 'spoiler-button--click-thru': uncached })}>
|
{(!visible || uncached) && (
|
||||||
|
<div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
|
{(visible && !uncached) && (
|
||||||
|
<div className='media-gallery__actions'>
|
||||||
|
<button className='media-gallery__actions__pill' onClick={this.handleOpen}><FormattedMessage id='media_gallery.hide' defaultMessage='Hide' /></button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(MediaGallery);
|
export default MediaGallery;
|
||||||
|
|
|
@ -2,14 +2,12 @@ import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { IconLogo } from 'mastodon/components/logo';
|
||||||
import { AuthorLink } from 'mastodon/features/explore/components/author_link';
|
import { AuthorLink } from 'mastodon/features/explore/components/author_link';
|
||||||
|
|
||||||
export const MoreFromAuthor = ({ accountId }) => (
|
export const MoreFromAuthor = ({ accountId }) => (
|
||||||
<div className='more-from-author'>
|
<div className='more-from-author'>
|
||||||
<svg viewBox='0 0 79 79' className='logo logo--icon' role='img'>
|
<IconLogo />
|
||||||
<use xlinkHref='#logo-symbol-icon' />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
|
<FormattedMessage id='link_preview.more_from_author' defaultMessage='More from {name}' values={{ name: <AuthorLink accountId={accountId} /> }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,7 +12,6 @@ import { HotKeys } from 'react-hotkeys';
|
||||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||||
import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
|
import PushPinIcon from '@/material-icons/400-24px/push_pin.svg?react';
|
||||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
|
||||||
import { ContentWarning } from 'mastodon/components/content_warning';
|
import { ContentWarning } from 'mastodon/components/content_warning';
|
||||||
import { FilterWarning } from 'mastodon/components/filter_warning';
|
import { FilterWarning } from 'mastodon/components/filter_warning';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
@ -34,6 +33,7 @@ import { getHashtagBarForStatus } from './hashtag_bar';
|
||||||
import { RelativeTimestamp } from './relative_timestamp';
|
import { RelativeTimestamp } from './relative_timestamp';
|
||||||
import StatusActionBar from './status_action_bar';
|
import StatusActionBar from './status_action_bar';
|
||||||
import StatusContent from './status_content';
|
import StatusContent from './status_content';
|
||||||
|
import { StatusThreadLabel } from './status_thread_label';
|
||||||
import { VisibilityIcon } from './visibility_icon';
|
import { VisibilityIcon } from './visibility_icon';
|
||||||
|
|
||||||
const domParser = new DOMParser();
|
const domParser = new DOMParser();
|
||||||
|
@ -413,7 +413,7 @@ class Status extends ImmutablePureComponent {
|
||||||
if (featured) {
|
if (featured) {
|
||||||
prepend = (
|
prepend = (
|
||||||
<div className='status__prepend'>
|
<div className='status__prepend'>
|
||||||
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' icon={PushPinIcon} className='status__prepend-icon' /></div>
|
<div className='status__prepend__icon'><Icon id='thumb-tack' icon={PushPinIcon} /></div>
|
||||||
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
|
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -422,7 +422,7 @@ class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
prepend = (
|
prepend = (
|
||||||
<div className='status__prepend'>
|
<div className='status__prepend'>
|
||||||
<div className='status__prepend-icon-wrapper'><Icon id='retweet' icon={RepeatIcon} className='status__prepend-icon' /></div>
|
<div className='status__prepend__icon'><Icon id='retweet' icon={RepeatIcon} /></div>
|
||||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -434,18 +434,13 @@ class Status extends ImmutablePureComponent {
|
||||||
} else if (status.get('visibility') === 'direct') {
|
} else if (status.get('visibility') === 'direct') {
|
||||||
prepend = (
|
prepend = (
|
||||||
<div className='status__prepend'>
|
<div className='status__prepend'>
|
||||||
<div className='status__prepend-icon-wrapper'><Icon id='at' icon={AlternateEmailIcon} className='status__prepend-icon' /></div>
|
<div className='status__prepend__icon'><Icon id='at' icon={AlternateEmailIcon} /></div>
|
||||||
<FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' />
|
<FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id'])) {
|
} else if (showThread && status.get('in_reply_to_id')) {
|
||||||
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
|
|
||||||
|
|
||||||
prepend = (
|
prepend = (
|
||||||
<div className='status__prepend'>
|
<StatusThreadLabel accountId={status.getIn(['account', 'id'])} inReplyToAccountId={status.get('in_reply_to_account_id')} />
|
||||||
<div className='status__prepend-icon-wrapper'><Icon id='reply' icon={ReplyIcon} className='status__prepend-icon' /></div>
|
|
||||||
<FormattedMessage id='status.replied_to' defaultMessage='Replied to {name}' values={{ name: <a onClick={this.handlePrependAccountClick} data-id={status.getIn(['account', 'id'])} data-hover-card-account={status.getIn(['account', 'id'])} href={`/@${status.getIn(['account', 'acct'])}`} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ const messages = defineMessages({
|
||||||
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' },
|
||||||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||||
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
||||||
embed: { id: 'status.embed', defaultMessage: 'Embed' },
|
embed: { id: 'status.embed', defaultMessage: 'Get embed code' },
|
||||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
|
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
|
||||||
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
|
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
|
||||||
|
|
50
app/javascript/mastodon/components/status_thread_label.tsx
Normal file
50
app/javascript/mastodon/components/status_thread_label.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { DisplayedName } from 'mastodon/features/notifications_v2/components/displayed_name';
|
||||||
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
export const StatusThreadLabel: React.FC<{
|
||||||
|
accountId: string;
|
||||||
|
inReplyToAccountId: string;
|
||||||
|
}> = ({ accountId, inReplyToAccountId }) => {
|
||||||
|
const inReplyToAccount = useAppSelector((state) =>
|
||||||
|
state.accounts.get(inReplyToAccountId),
|
||||||
|
);
|
||||||
|
|
||||||
|
let label;
|
||||||
|
|
||||||
|
if (accountId === inReplyToAccountId) {
|
||||||
|
label = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.continued_thread'
|
||||||
|
defaultMessage='Continued thread'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (inReplyToAccount) {
|
||||||
|
label = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.replied_to'
|
||||||
|
defaultMessage='Replied to {name}'
|
||||||
|
values={{ name: <DisplayedName accountIds={[inReplyToAccountId]} /> }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
label = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.replied_in_thread'
|
||||||
|
defaultMessage='Replied in thread'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='status__prepend'>
|
||||||
|
<div className='status__prepend__icon'>
|
||||||
|
<Icon id='reply' icon={ReplyIcon} />
|
||||||
|
</div>
|
||||||
|
{label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -6,7 +6,6 @@ import {
|
||||||
unmuteAccount,
|
unmuteAccount,
|
||||||
unblockAccount,
|
unblockAccount,
|
||||||
} from '../actions/accounts';
|
} from '../actions/accounts';
|
||||||
import { showAlertForError } from '../actions/alerts';
|
|
||||||
import { initBlockModal } from '../actions/blocks';
|
import { initBlockModal } from '../actions/blocks';
|
||||||
import {
|
import {
|
||||||
replyCompose,
|
replyCompose,
|
||||||
|
@ -100,10 +99,7 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
|
||||||
onEmbed (status) {
|
onEmbed (status) {
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalType: 'EMBED',
|
modalType: 'EMBED',
|
||||||
modalProps: {
|
modalProps: { id: status.get('id') },
|
||||||
id: status.get('id'),
|
|
||||||
onError: error => dispatch(showAlertForError(error)),
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -170,7 +170,7 @@ export const Conversation = ({ conversation, scrollKey, onMoveUp, onMoveDown })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HotKeys handlers={handlers}>
|
<HotKeys handlers={handlers}>
|
||||||
<div className={classNames('conversation focusable muted', { 'conversation--unread': unread })} tabIndex={0}>
|
<div className={classNames('conversation focusable muted', { unread })} tabIndex={0}>
|
||||||
<div className='conversation__avatar' onClick={handleClick} role='presentation'>
|
<div className='conversation__avatar' onClick={handleClick} role='presentation'>
|
||||||
<AvatarComposite accounts={accounts} size={48} />
|
<AvatarComposite accounts={accounts} size={48} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -131,7 +131,7 @@ class LoginForm extends React.PureComponent {
|
||||||
try {
|
try {
|
||||||
new URL(url);
|
new URL(url);
|
||||||
return true;
|
return true;
|
||||||
} catch(_) {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -42,19 +42,11 @@ export const NotificationAdminReport: React.FC<{
|
||||||
|
|
||||||
if (!account || !targetAccount) return null;
|
if (!account || !targetAccount) return null;
|
||||||
|
|
||||||
|
const domain = account.acct.split('@')[1];
|
||||||
|
|
||||||
const values = {
|
const values = {
|
||||||
name: (
|
name: <bdi>{domain ?? `@${account.acct}`}</bdi>,
|
||||||
<bdi
|
target: <bdi>@{targetAccount.acct}</bdi>,
|
||||||
dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
target: (
|
|
||||||
<bdi
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: targetAccount.get('display_name_html'),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
category: intl.formatMessage(messages[report.category]),
|
category: intl.formatMessage(messages[report.category]),
|
||||||
count: report.status_ids.length,
|
count: report.status_ids.length,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
|
||||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||||
import ReplyIcon from '@/material-icons/400-24px/reply-fill.svg?react';
|
import ReplyIcon from '@/material-icons/400-24px/reply-fill.svg?react';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
|
@ -47,7 +49,7 @@ export const NotificationMention: React.FC<{
|
||||||
status.get('visibility') === 'direct',
|
status.get('visibility') === 'direct',
|
||||||
status.get('in_reply_to_account_id') === me,
|
status.get('in_reply_to_account_id') === me,
|
||||||
] as const;
|
] as const;
|
||||||
});
|
}, isEqual);
|
||||||
|
|
||||||
let labelRenderer = mentionLabelRenderer;
|
let labelRenderer = mentionLabelRenderer;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
|
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
|
||||||
import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
|
import DoneAllIcon from '@/material-icons/400-24px/done_all.svg?react';
|
||||||
|
@ -62,7 +63,7 @@ export const Notifications: React.FC<{
|
||||||
multiColumn?: boolean;
|
multiColumn?: boolean;
|
||||||
}> = ({ columnId, multiColumn }) => {
|
}> = ({ columnId, multiColumn }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const notifications = useAppSelector(selectNotificationGroups);
|
const notifications = useAppSelector(selectNotificationGroups, isEqual);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isLoading = useAppSelector((s) => s.notificationGroups.isLoading);
|
const isLoading = useAppSelector((s) => s.notificationGroups.isLoading);
|
||||||
const hasMore = notifications.at(-1)?.type === 'gap';
|
const hasMore = notifications.at(-1)?.type === 'gap';
|
||||||
|
|
|
@ -10,8 +10,8 @@ import { Link } from 'react-router-dom';
|
||||||
import SwipeableViews from 'react-swipeable-views';
|
import SwipeableViews from 'react-swipeable-views';
|
||||||
|
|
||||||
import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
|
import ArrowRightAltIcon from '@/material-icons/400-24px/arrow_right_alt.svg?react';
|
||||||
import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
|
|
||||||
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
||||||
|
import { CopyPasteText } from 'mastodon/components/copy_paste_text';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { me, domain } from 'mastodon/initial_state';
|
import { me, domain } from 'mastodon/initial_state';
|
||||||
import { useAppSelector } from 'mastodon/store';
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
@ -20,67 +20,6 @@ const messages = defineMessages({
|
||||||
shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
|
shareableMessage: { id: 'onboarding.share.message', defaultMessage: 'I\'m {username} on #Mastodon! Come follow me at {url}' },
|
||||||
});
|
});
|
||||||
|
|
||||||
class CopyPasteText extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
copied: false,
|
|
||||||
focused: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
setRef = c => {
|
|
||||||
this.input = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleInputClick = () => {
|
|
||||||
this.setState({ copied: false });
|
|
||||||
this.input.focus();
|
|
||||||
this.input.select();
|
|
||||||
this.input.setSelectionRange(0, this.props.value.length);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleButtonClick = e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
const { value } = this.props;
|
|
||||||
navigator.clipboard.writeText(value);
|
|
||||||
this.input.blur();
|
|
||||||
this.setState({ copied: true });
|
|
||||||
this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleFocus = () => {
|
|
||||||
this.setState({ focused: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
handleBlur = () => {
|
|
||||||
this.setState({ focused: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
if (this.timeout) clearTimeout(this.timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { value } = this.props;
|
|
||||||
const { copied, focused } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames('copy-paste-text', { copied, focused })} tabIndex='0' role='button' onClick={this.handleInputClick}>
|
|
||||||
<textarea readOnly value={value} ref={this.setRef} onClick={this.handleInputClick} onFocus={this.handleFocus} onBlur={this.handleBlur} />
|
|
||||||
|
|
||||||
<button className='button' onClick={this.handleButtonClick}>
|
|
||||||
<Icon id='copy' icon={ContentCopyIcon} /> {copied ? <FormattedMessage id='copypaste.copied' defaultMessage='Copied' /> : <FormattedMessage id='copypaste.copy_to_clipboard' defaultMessage='Copy to clipboard' />}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class TipCarousel extends PureComponent {
|
class TipCarousel extends PureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|
87
app/javascript/mastodon/features/standalone/status/index.tsx
Normal file
87
app/javascript/mastodon/features/standalone/status/index.tsx
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-return,
|
||||||
|
@typescript-eslint/no-explicit-any,
|
||||||
|
@typescript-eslint/no-unsafe-assignment */
|
||||||
|
|
||||||
|
import { useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
|
import { useRenderSignal } from 'mastodon/../hooks/useRenderSignal';
|
||||||
|
import { fetchStatus, toggleStatusSpoilers } from 'mastodon/actions/statuses';
|
||||||
|
import { hydrateStore } from 'mastodon/actions/store';
|
||||||
|
import { Router } from 'mastodon/components/router';
|
||||||
|
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
|
||||||
|
import initialState from 'mastodon/initial_state';
|
||||||
|
import { IntlProvider } from 'mastodon/locales';
|
||||||
|
import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
|
||||||
|
import { store, useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
|
||||||
|
const getPictureInPicture = makeGetPictureInPicture() as unknown as (
|
||||||
|
arg0: any,
|
||||||
|
arg1: any,
|
||||||
|
) => any;
|
||||||
|
|
||||||
|
const Embed: React.FC<{ id: string }> = ({ id }) => {
|
||||||
|
const status = useAppSelector((state) => getStatus(state, { id }));
|
||||||
|
const pictureInPicture = useAppSelector((state) =>
|
||||||
|
getPictureInPicture(state, { id }),
|
||||||
|
);
|
||||||
|
const domain = useAppSelector((state) => state.meta.get('domain'));
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const dispatchRenderSignal = useRenderSignal();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchStatus(id, false, false));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
const handleToggleHidden = useCallback(() => {
|
||||||
|
dispatch(toggleStatusSpoilers(id));
|
||||||
|
}, [dispatch, id]);
|
||||||
|
|
||||||
|
// This allows us to calculate the correct page height for embeds
|
||||||
|
if (status) {
|
||||||
|
dispatchRenderSignal();
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||||
|
const permalink = status?.get('url') as string;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='embed'>
|
||||||
|
<DetailedStatus
|
||||||
|
status={status}
|
||||||
|
domain={domain}
|
||||||
|
pictureInPicture={pictureInPicture}
|
||||||
|
onToggleHidden={handleToggleHidden}
|
||||||
|
withLogo
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a
|
||||||
|
className='embed__overlay'
|
||||||
|
href={permalink}
|
||||||
|
target='_blank'
|
||||||
|
rel='noreferrer noopener'
|
||||||
|
aria-label=''
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Status: React.FC<{ id: string }> = ({ id }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialState) {
|
||||||
|
store.dispatch(hydrateStore(initialState));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntlProvider>
|
||||||
|
<Provider store={store}>
|
||||||
|
<Router>
|
||||||
|
<Embed id={id} />
|
||||||
|
</Router>
|
||||||
|
</Provider>
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
};
|
|
@ -49,7 +49,7 @@ const messages = defineMessages({
|
||||||
share: { id: 'status.share', defaultMessage: 'Share' },
|
share: { id: 'status.share', defaultMessage: 'Share' },
|
||||||
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
pin: { id: 'status.pin', defaultMessage: 'Pin on profile' },
|
||||||
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' },
|
||||||
embed: { id: 'status.embed', defaultMessage: 'Embed' },
|
embed: { id: 'status.embed', defaultMessage: 'Get embed code' },
|
||||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||||
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
|
admin_status: { id: 'status.admin_status', defaultMessage: 'Open this post in the moderation interface' },
|
||||||
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
|
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' },
|
||||||
|
|
|
@ -1,322 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { FormattedDate, FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
|
||||||
import { AnimatedNumber } from 'mastodon/components/animated_number';
|
|
||||||
import { ContentWarning } from 'mastodon/components/content_warning';
|
|
||||||
import EditedTimestamp from 'mastodon/components/edited_timestamp';
|
|
||||||
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
|
|
||||||
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
|
|
||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
|
||||||
|
|
||||||
import { Avatar } from '../../../components/avatar';
|
|
||||||
import { DisplayName } from '../../../components/display_name';
|
|
||||||
import MediaGallery from '../../../components/media_gallery';
|
|
||||||
import StatusContent from '../../../components/status_content';
|
|
||||||
import Audio from '../../audio';
|
|
||||||
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
|
|
||||||
import Video from '../../video';
|
|
||||||
|
|
||||||
import Card from './card';
|
|
||||||
|
|
||||||
class DetailedStatus extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
status: ImmutablePropTypes.map,
|
|
||||||
onOpenMedia: PropTypes.func.isRequired,
|
|
||||||
onOpenVideo: PropTypes.func.isRequired,
|
|
||||||
onToggleHidden: PropTypes.func.isRequired,
|
|
||||||
onTranslate: PropTypes.func.isRequired,
|
|
||||||
measureHeight: PropTypes.bool,
|
|
||||||
onHeightChange: PropTypes.func,
|
|
||||||
domain: PropTypes.string.isRequired,
|
|
||||||
compact: PropTypes.bool,
|
|
||||||
showMedia: PropTypes.bool,
|
|
||||||
pictureInPicture: ImmutablePropTypes.contains({
|
|
||||||
inUse: PropTypes.bool,
|
|
||||||
available: PropTypes.bool,
|
|
||||||
}),
|
|
||||||
onToggleMediaVisibility: PropTypes.func,
|
|
||||||
...WithRouterPropTypes,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
height: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAccountClick = (e) => {
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && this.props.history) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleOpenVideo = (options) => {
|
|
||||||
this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleExpandedToggle = () => {
|
|
||||||
this.props.onToggleHidden(this.props.status);
|
|
||||||
};
|
|
||||||
|
|
||||||
_measureHeight (heightJustChanged) {
|
|
||||||
if (this.props.measureHeight && this.node) {
|
|
||||||
scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 }));
|
|
||||||
|
|
||||||
if (this.props.onHeightChange && heightJustChanged) {
|
|
||||||
this.props.onHeightChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setRef = c => {
|
|
||||||
this.node = c;
|
|
||||||
this._measureHeight();
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidUpdate (prevProps, prevState) {
|
|
||||||
this._measureHeight(prevState.height !== this.state.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleModalLink = e => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
let href;
|
|
||||||
|
|
||||||
if (e.target.nodeName !== 'A') {
|
|
||||||
href = e.target.parentNode.href;
|
|
||||||
} else {
|
|
||||||
href = e.target.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
|
|
||||||
};
|
|
||||||
|
|
||||||
handleTranslate = () => {
|
|
||||||
const { onTranslate, status } = this.props;
|
|
||||||
onTranslate(status);
|
|
||||||
};
|
|
||||||
|
|
||||||
_properStatus () {
|
|
||||||
const { status } = this.props;
|
|
||||||
|
|
||||||
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
|
|
||||||
return status.get('reblog');
|
|
||||||
} else {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getAttachmentAspectRatio () {
|
|
||||||
const attachments = this._properStatus().get('media_attachments');
|
|
||||||
|
|
||||||
if (attachments.getIn([0, 'type']) === 'video') {
|
|
||||||
return `${attachments.getIn([0, 'meta', 'original', 'width'])} / ${attachments.getIn([0, 'meta', 'original', 'height'])}`;
|
|
||||||
} else if (attachments.getIn([0, 'type']) === 'audio') {
|
|
||||||
return '16 / 9';
|
|
||||||
} else {
|
|
||||||
return (attachments.size === 1 && attachments.getIn([0, 'meta', 'small', 'aspect'])) ? attachments.getIn([0, 'meta', 'small', 'aspect']) : '3 / 2';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const status = this._properStatus();
|
|
||||||
const outerStyle = { boxSizing: 'border-box' };
|
|
||||||
const { compact, pictureInPicture } = this.props;
|
|
||||||
|
|
||||||
if (!status) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let media = '';
|
|
||||||
let applicationLink = '';
|
|
||||||
let reblogLink = '';
|
|
||||||
let favouriteLink = '';
|
|
||||||
|
|
||||||
if (this.props.measureHeight) {
|
|
||||||
outerStyle.height = `${this.state.height}px`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
|
||||||
|
|
||||||
if (pictureInPicture.get('inUse')) {
|
|
||||||
media = <PictureInPicturePlaceholder aspectRatio={this.getAttachmentAspectRatio()} />;
|
|
||||||
} else if (status.get('media_attachments').size > 0) {
|
|
||||||
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
|
||||||
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
|
||||||
|
|
||||||
media = (
|
|
||||||
<Audio
|
|
||||||
src={attachment.get('url')}
|
|
||||||
alt={description}
|
|
||||||
lang={language}
|
|
||||||
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
|
||||||
poster={attachment.get('preview_url') || status.getIn(['account', 'avatar_static'])}
|
|
||||||
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
|
||||||
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
|
||||||
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
visible={this.props.showMedia}
|
|
||||||
blurhash={attachment.get('blurhash')}
|
|
||||||
height={150}
|
|
||||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
|
||||||
const attachment = status.getIn(['media_attachments', 0]);
|
|
||||||
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
|
||||||
|
|
||||||
media = (
|
|
||||||
<Video
|
|
||||||
preview={attachment.get('preview_url')}
|
|
||||||
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
|
||||||
aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`}
|
|
||||||
blurhash={attachment.get('blurhash')}
|
|
||||||
src={attachment.get('url')}
|
|
||||||
alt={description}
|
|
||||||
lang={language}
|
|
||||||
width={300}
|
|
||||||
height={150}
|
|
||||||
onOpenVideo={this.handleOpenVideo}
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
visible={this.props.showMedia}
|
|
||||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
media = (
|
|
||||||
<MediaGallery
|
|
||||||
standalone
|
|
||||||
sensitive={status.get('sensitive')}
|
|
||||||
media={status.get('media_attachments')}
|
|
||||||
lang={language}
|
|
||||||
height={300}
|
|
||||||
onOpenMedia={this.props.onOpenMedia}
|
|
||||||
visible={this.props.showMedia}
|
|
||||||
onToggleVisibility={this.props.onToggleMediaVisibility}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (status.get('spoiler_text').length === 0) {
|
|
||||||
media = <Card sensitive={status.get('sensitive')} onOpenMedia={this.props.onOpenMedia} card={status.get('card', null)} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.get('application')) {
|
|
||||||
applicationLink = <>·<a className='detailed-status__application' href={status.getIn(['application', 'website'])} target='_blank' rel='noopener noreferrer'>{status.getIn(['application', 'name'])}</a></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const visibilityLink = <>·<VisibilityIcon visibility={status.get('visibility')} /></>;
|
|
||||||
|
|
||||||
if (['private', 'direct'].includes(status.get('visibility'))) {
|
|
||||||
reblogLink = '';
|
|
||||||
} else if (this.props.history) {
|
|
||||||
reblogLink = (
|
|
||||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`} className='detailed-status__link'>
|
|
||||||
<span className='detailed-status__reblogs'>
|
|
||||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
|
||||||
</span>
|
|
||||||
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
reblogLink = (
|
|
||||||
<a href={`/interact/${status.get('id')}?type=reblog`} className='detailed-status__link' onClick={this.handleModalLink}>
|
|
||||||
<span className='detailed-status__reblogs'>
|
|
||||||
<AnimatedNumber value={status.get('reblogs_count')} />
|
|
||||||
</span>
|
|
||||||
<FormattedMessage id='status.reblogs' defaultMessage='{count, plural, one {boost} other {boosts}}' values={{ count: status.get('reblogs_count') }} />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.history) {
|
|
||||||
favouriteLink = (
|
|
||||||
<Link to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`} className='detailed-status__link'>
|
|
||||||
<span className='detailed-status__favorites'>
|
|
||||||
<AnimatedNumber value={status.get('favourites_count')} />
|
|
||||||
</span>
|
|
||||||
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
favouriteLink = (
|
|
||||||
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
|
|
||||||
<span className='detailed-status__favorites'>
|
|
||||||
<AnimatedNumber value={status.get('favourites_count')} />
|
|
||||||
</span>
|
|
||||||
<FormattedMessage id='status.favourites' defaultMessage='{count, plural, one {favorite} other {favorites}}' values={{ count: status.get('favourites_count') }} />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const {statusContentProps, hashtagBar} = getHashtagBarForStatus(status);
|
|
||||||
const expanded = !status.get('hidden') || status.get('spoiler_text').length === 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={outerStyle}>
|
|
||||||
<div ref={this.setRef} className={classNames('detailed-status', { compact })}>
|
|
||||||
{status.get('visibility') === 'direct' && (
|
|
||||||
<div className='status__prepend'>
|
|
||||||
<div className='status__prepend-icon-wrapper'><Icon id='at' icon={AlternateEmailIcon} className='status__prepend-icon' /></div>
|
|
||||||
<FormattedMessage id='status.direct_indicator' defaultMessage='Private mention' />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<a href={`/@${status.getIn(['account', 'acct'])}`} data-hover-card-account={status.getIn(['account', 'id'])} onClick={this.handleAccountClick} className='detailed-status__display-name'>
|
|
||||||
<div className='detailed-status__display-avatar'><Avatar account={status.get('account')} size={46} /></div>
|
|
||||||
<DisplayName account={status.get('account')} localDomain={this.props.domain} />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{status.get('spoiler_text').length > 0 && <ContentWarning text={status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml')} expanded={expanded} onClick={this.handleExpandedToggle} />}
|
|
||||||
|
|
||||||
{expanded && (
|
|
||||||
<>
|
|
||||||
<StatusContent
|
|
||||||
status={status}
|
|
||||||
onTranslate={this.handleTranslate}
|
|
||||||
{...statusContentProps}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{media}
|
|
||||||
{hashtagBar}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className='detailed-status__meta'>
|
|
||||||
<div className='detailed-status__meta__line'>
|
|
||||||
<a className='detailed-status__datetime' href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} target='_blank' rel='noopener noreferrer'>
|
|
||||||
<FormattedDate value={new Date(status.get('created_at'))} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
|
|
||||||
</a>
|
|
||||||
|
|
||||||
{visibilityLink}
|
|
||||||
|
|
||||||
{applicationLink}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{status.get('edited_at') && <div className='detailed-status__meta__line'><EditedTimestamp statusId={status.get('id')} timestamp={status.get('edited_at')} /></div>}
|
|
||||||
|
|
||||||
<div className='detailed-status__meta__line'>
|
|
||||||
{reblogLink}
|
|
||||||
{reblogLink && <>·</>}
|
|
||||||
{favouriteLink}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(DetailedStatus);
|
|
|
@ -0,0 +1,390 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access,
|
||||||
|
@typescript-eslint/no-unsafe-call,
|
||||||
|
@typescript-eslint/no-explicit-any,
|
||||||
|
@typescript-eslint/no-unsafe-assignment */
|
||||||
|
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
import { useState, useRef, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { FormattedDate, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||||
|
import { AnimatedNumber } from 'mastodon/components/animated_number';
|
||||||
|
import { ContentWarning } from 'mastodon/components/content_warning';
|
||||||
|
import EditedTimestamp from 'mastodon/components/edited_timestamp';
|
||||||
|
import type { StatusLike } from 'mastodon/components/hashtag_bar';
|
||||||
|
import { getHashtagBarForStatus } from 'mastodon/components/hashtag_bar';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { IconLogo } from 'mastodon/components/logo';
|
||||||
|
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
|
||||||
|
import { VisibilityIcon } from 'mastodon/components/visibility_icon';
|
||||||
|
|
||||||
|
import { Avatar } from '../../../components/avatar';
|
||||||
|
import { DisplayName } from '../../../components/display_name';
|
||||||
|
import MediaGallery from '../../../components/media_gallery';
|
||||||
|
import StatusContent from '../../../components/status_content';
|
||||||
|
import Audio from '../../audio';
|
||||||
|
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
|
||||||
|
import Video from '../../video';
|
||||||
|
|
||||||
|
import Card from './card';
|
||||||
|
|
||||||
|
interface VideoModalOptions {
|
||||||
|
startTime: number;
|
||||||
|
autoPlay?: boolean;
|
||||||
|
defaultVolume: number;
|
||||||
|
componentIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DetailedStatus: React.FC<{
|
||||||
|
status: any;
|
||||||
|
onOpenMedia?: (status: any, index: number, lang: string) => void;
|
||||||
|
onOpenVideo?: (status: any, lang: string, options: VideoModalOptions) => void;
|
||||||
|
onTranslate?: (status: any) => void;
|
||||||
|
measureHeight?: boolean;
|
||||||
|
onHeightChange?: () => void;
|
||||||
|
domain: string;
|
||||||
|
showMedia?: boolean;
|
||||||
|
withLogo?: boolean;
|
||||||
|
pictureInPicture: any;
|
||||||
|
onToggleHidden?: (status: any) => void;
|
||||||
|
onToggleMediaVisibility?: () => void;
|
||||||
|
}> = ({
|
||||||
|
status,
|
||||||
|
onOpenMedia,
|
||||||
|
onOpenVideo,
|
||||||
|
onTranslate,
|
||||||
|
measureHeight,
|
||||||
|
onHeightChange,
|
||||||
|
domain,
|
||||||
|
showMedia,
|
||||||
|
withLogo,
|
||||||
|
pictureInPicture,
|
||||||
|
onToggleMediaVisibility,
|
||||||
|
onToggleHidden,
|
||||||
|
}) => {
|
||||||
|
const properStatus = status?.get('reblog') ?? status;
|
||||||
|
const [height, setHeight] = useState(0);
|
||||||
|
const nodeRef = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
const handleOpenVideo = useCallback(
|
||||||
|
(options: VideoModalOptions) => {
|
||||||
|
const lang = (status.getIn(['translation', 'language']) ||
|
||||||
|
status.get('language')) as string;
|
||||||
|
if (onOpenVideo)
|
||||||
|
onOpenVideo(status.getIn(['media_attachments', 0]), lang, options);
|
||||||
|
},
|
||||||
|
[onOpenVideo, status],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleExpandedToggle = useCallback(() => {
|
||||||
|
if (onToggleHidden) onToggleHidden(status);
|
||||||
|
}, [onToggleHidden, status]);
|
||||||
|
|
||||||
|
const _measureHeight = useCallback(
|
||||||
|
(heightJustChanged?: boolean) => {
|
||||||
|
if (measureHeight && nodeRef.current) {
|
||||||
|
scheduleIdleTask(() => {
|
||||||
|
if (nodeRef.current)
|
||||||
|
setHeight(Math.ceil(nodeRef.current.scrollHeight) + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onHeightChange && heightJustChanged) {
|
||||||
|
onHeightChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onHeightChange, measureHeight, setHeight],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRef = useCallback(
|
||||||
|
(c: HTMLDivElement) => {
|
||||||
|
nodeRef.current = c;
|
||||||
|
_measureHeight();
|
||||||
|
},
|
||||||
|
[_measureHeight],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTranslate = useCallback(() => {
|
||||||
|
if (onTranslate) onTranslate(status);
|
||||||
|
}, [onTranslate, status]);
|
||||||
|
|
||||||
|
if (!properStatus) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let media;
|
||||||
|
let applicationLink;
|
||||||
|
let reblogLink;
|
||||||
|
let attachmentAspectRatio;
|
||||||
|
|
||||||
|
if (properStatus.get('media_attachments').getIn([0, 'type']) === 'video') {
|
||||||
|
attachmentAspectRatio = `${properStatus.get('media_attachments').getIn([0, 'meta', 'original', 'width'])} / ${properStatus.get('media_attachments').getIn([0, 'meta', 'original', 'height'])}`;
|
||||||
|
} else if (
|
||||||
|
properStatus.get('media_attachments').getIn([0, 'type']) === 'audio'
|
||||||
|
) {
|
||||||
|
attachmentAspectRatio = '16 / 9';
|
||||||
|
} else {
|
||||||
|
attachmentAspectRatio =
|
||||||
|
properStatus.get('media_attachments').size === 1 &&
|
||||||
|
properStatus
|
||||||
|
.get('media_attachments')
|
||||||
|
.getIn([0, 'meta', 'small', 'aspect'])
|
||||||
|
? properStatus
|
||||||
|
.get('media_attachments')
|
||||||
|
.getIn([0, 'meta', 'small', 'aspect'])
|
||||||
|
: '3 / 2';
|
||||||
|
}
|
||||||
|
|
||||||
|
const outerStyle = { boxSizing: 'border-box' } as CSSProperties;
|
||||||
|
|
||||||
|
if (measureHeight) {
|
||||||
|
outerStyle.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
const language =
|
||||||
|
status.getIn(['translation', 'language']) || status.get('language');
|
||||||
|
|
||||||
|
if (pictureInPicture.get('inUse')) {
|
||||||
|
media = <PictureInPicturePlaceholder aspectRatio={attachmentAspectRatio} />;
|
||||||
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
|
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||||
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
const description =
|
||||||
|
attachment.getIn(['translation', 'description']) ||
|
||||||
|
attachment.get('description');
|
||||||
|
|
||||||
|
media = (
|
||||||
|
<Audio
|
||||||
|
src={attachment.get('url')}
|
||||||
|
alt={description}
|
||||||
|
lang={language}
|
||||||
|
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
|
||||||
|
poster={
|
||||||
|
attachment.get('preview_url') ||
|
||||||
|
status.getIn(['account', 'avatar_static'])
|
||||||
|
}
|
||||||
|
backgroundColor={attachment.getIn(['meta', 'colors', 'background'])}
|
||||||
|
foregroundColor={attachment.getIn(['meta', 'colors', 'foreground'])}
|
||||||
|
accentColor={attachment.getIn(['meta', 'colors', 'accent'])}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
visible={showMedia}
|
||||||
|
blurhash={attachment.get('blurhash')}
|
||||||
|
height={150}
|
||||||
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
|
||||||
|
const attachment = status.getIn(['media_attachments', 0]);
|
||||||
|
const description =
|
||||||
|
attachment.getIn(['translation', 'description']) ||
|
||||||
|
attachment.get('description');
|
||||||
|
|
||||||
|
media = (
|
||||||
|
<Video
|
||||||
|
preview={attachment.get('preview_url')}
|
||||||
|
frameRate={attachment.getIn(['meta', 'original', 'frame_rate'])}
|
||||||
|
aspectRatio={`${attachment.getIn(['meta', 'original', 'width'])} / ${attachment.getIn(['meta', 'original', 'height'])}`}
|
||||||
|
blurhash={attachment.get('blurhash')}
|
||||||
|
src={attachment.get('url')}
|
||||||
|
alt={description}
|
||||||
|
lang={language}
|
||||||
|
width={300}
|
||||||
|
height={150}
|
||||||
|
onOpenVideo={handleOpenVideo}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
visible={showMedia}
|
||||||
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
media = (
|
||||||
|
<MediaGallery
|
||||||
|
standalone
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
lang={language}
|
||||||
|
height={300}
|
||||||
|
onOpenMedia={onOpenMedia}
|
||||||
|
visible={showMedia}
|
||||||
|
onToggleVisibility={onToggleMediaVisibility}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (status.get('spoiler_text').length === 0) {
|
||||||
|
media = (
|
||||||
|
<Card
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
onOpenMedia={onOpenMedia}
|
||||||
|
card={status.get('card', null)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.get('application')) {
|
||||||
|
applicationLink = (
|
||||||
|
<>
|
||||||
|
·
|
||||||
|
<a
|
||||||
|
className='detailed-status__application'
|
||||||
|
href={status.getIn(['application', 'website'])}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
{status.getIn(['application', 'name'])}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibilityLink = (
|
||||||
|
<>
|
||||||
|
·<VisibilityIcon visibility={status.get('visibility')} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (['private', 'direct'].includes(status.get('visibility') as string)) {
|
||||||
|
reblogLink = '';
|
||||||
|
} else {
|
||||||
|
reblogLink = (
|
||||||
|
<Link
|
||||||
|
to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/reblogs`}
|
||||||
|
className='detailed-status__link'
|
||||||
|
>
|
||||||
|
<span className='detailed-status__reblogs'>
|
||||||
|
<AnimatedNumber value={status.get('reblogs_count')} />
|
||||||
|
</span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.reblogs'
|
||||||
|
defaultMessage='{count, plural, one {boost} other {boosts}}'
|
||||||
|
values={{ count: status.get('reblogs_count') }}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const favouriteLink = (
|
||||||
|
<Link
|
||||||
|
to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/favourites`}
|
||||||
|
className='detailed-status__link'
|
||||||
|
>
|
||||||
|
<span className='detailed-status__favorites'>
|
||||||
|
<AnimatedNumber value={status.get('favourites_count')} />
|
||||||
|
</span>
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.favourites'
|
||||||
|
defaultMessage='{count, plural, one {favorite} other {favorites}}'
|
||||||
|
values={{ count: status.get('favourites_count') }}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
const { statusContentProps, hashtagBar } = getHashtagBarForStatus(
|
||||||
|
status as StatusLike,
|
||||||
|
);
|
||||||
|
const expanded =
|
||||||
|
!status.get('hidden') || status.get('spoiler_text').length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={outerStyle}>
|
||||||
|
<div ref={handleRef} className={classNames('detailed-status')}>
|
||||||
|
{status.get('visibility') === 'direct' && (
|
||||||
|
<div className='status__prepend'>
|
||||||
|
<div className='status__prepend-icon-wrapper'>
|
||||||
|
<Icon
|
||||||
|
id='at'
|
||||||
|
icon={AlternateEmailIcon}
|
||||||
|
className='status__prepend-icon'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<FormattedMessage
|
||||||
|
id='status.direct_indicator'
|
||||||
|
defaultMessage='Private mention'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Link
|
||||||
|
to={`/@${status.getIn(['account', 'acct'])}`}
|
||||||
|
data-hover-card-account={status.getIn(['account', 'id'])}
|
||||||
|
className='detailed-status__display-name'
|
||||||
|
>
|
||||||
|
<div className='detailed-status__display-avatar'>
|
||||||
|
<Avatar account={status.get('account')} size={46} />
|
||||||
|
</div>
|
||||||
|
<DisplayName account={status.get('account')} localDomain={domain} />
|
||||||
|
{withLogo && (
|
||||||
|
<>
|
||||||
|
<div className='spacer' />
|
||||||
|
<IconLogo />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{status.get('spoiler_text').length > 0 && (
|
||||||
|
<ContentWarning
|
||||||
|
text={
|
||||||
|
status.getIn(['translation', 'spoilerHtml']) ||
|
||||||
|
status.get('spoilerHtml')
|
||||||
|
}
|
||||||
|
expanded={expanded}
|
||||||
|
onClick={handleExpandedToggle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{expanded && (
|
||||||
|
<>
|
||||||
|
<StatusContent
|
||||||
|
status={status}
|
||||||
|
onTranslate={handleTranslate}
|
||||||
|
{...(statusContentProps as any)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{media}
|
||||||
|
{hashtagBar}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='detailed-status__meta'>
|
||||||
|
<div className='detailed-status__meta__line'>
|
||||||
|
<a
|
||||||
|
className='detailed-status__datetime'
|
||||||
|
href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`}
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
>
|
||||||
|
<FormattedDate
|
||||||
|
value={new Date(status.get('created_at') as string)}
|
||||||
|
year='numeric'
|
||||||
|
month='short'
|
||||||
|
day='2-digit'
|
||||||
|
hour='2-digit'
|
||||||
|
minute='2-digit'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{visibilityLink}
|
||||||
|
{applicationLink}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status.get('edited_at') && (
|
||||||
|
<div className='detailed-status__meta__line'>
|
||||||
|
<EditedTimestamp
|
||||||
|
statusId={status.get('id')}
|
||||||
|
timestamp={status.get('edited_at')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className='detailed-status__meta__line'>
|
||||||
|
{reblogLink}
|
||||||
|
{reblogLink && <>·</>}
|
||||||
|
{favouriteLink}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,140 +0,0 @@
|
||||||
import { injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { showAlertForError } from '../../../actions/alerts';
|
|
||||||
import { initBlockModal } from '../../../actions/blocks';
|
|
||||||
import {
|
|
||||||
replyCompose,
|
|
||||||
mentionCompose,
|
|
||||||
directCompose,
|
|
||||||
} from '../../../actions/compose';
|
|
||||||
import {
|
|
||||||
toggleReblog,
|
|
||||||
toggleFavourite,
|
|
||||||
pin,
|
|
||||||
unpin,
|
|
||||||
} from '../../../actions/interactions';
|
|
||||||
import { openModal } from '../../../actions/modal';
|
|
||||||
import { initMuteModal } from '../../../actions/mutes';
|
|
||||||
import { initReport } from '../../../actions/reports';
|
|
||||||
import {
|
|
||||||
muteStatus,
|
|
||||||
unmuteStatus,
|
|
||||||
deleteStatus,
|
|
||||||
toggleStatusSpoilers,
|
|
||||||
} from '../../../actions/statuses';
|
|
||||||
import { deleteModal } from '../../../initial_state';
|
|
||||||
import { makeGetStatus, makeGetPictureInPicture } from '../../../selectors';
|
|
||||||
import DetailedStatus from '../components/detailed_status';
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
|
||||||
const getStatus = makeGetStatus();
|
|
||||||
const getPictureInPicture = makeGetPictureInPicture();
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
|
||||||
status: getStatus(state, props),
|
|
||||||
domain: state.getIn(['meta', 'domain']),
|
|
||||||
pictureInPicture: getPictureInPicture(state, props),
|
|
||||||
});
|
|
||||||
|
|
||||||
return mapStateToProps;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
|
||||||
|
|
||||||
onReply (status) {
|
|
||||||
dispatch((_, getState) => {
|
|
||||||
let state = getState();
|
|
||||||
if (state.getIn(['compose', 'text']).trim().length !== 0) {
|
|
||||||
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } }));
|
|
||||||
} else {
|
|
||||||
dispatch(replyCompose(status));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
onReblog (status, e) {
|
|
||||||
dispatch(toggleReblog(status.get('id'), e.shiftKey));
|
|
||||||
},
|
|
||||||
|
|
||||||
onFavourite (status) {
|
|
||||||
dispatch(toggleFavourite(status.get('id')));
|
|
||||||
},
|
|
||||||
|
|
||||||
onPin (status) {
|
|
||||||
if (status.get('pinned')) {
|
|
||||||
dispatch(unpin(status));
|
|
||||||
} else {
|
|
||||||
dispatch(pin(status));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onEmbed (status) {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'EMBED',
|
|
||||||
modalProps: {
|
|
||||||
id: status.get('id'),
|
|
||||||
onError: error => dispatch(showAlertForError(error)),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onDelete (status, withRedraft = false) {
|
|
||||||
if (!deleteModal) {
|
|
||||||
dispatch(deleteStatus(status.get('id'), withRedraft));
|
|
||||||
} else {
|
|
||||||
dispatch(openModal({ modalType: 'CONFIRM_DELETE_STATUS', modalProps: { statusId: status.get('id'), withRedraft } }));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onDirect (account) {
|
|
||||||
dispatch(directCompose(account));
|
|
||||||
},
|
|
||||||
|
|
||||||
onMention (account) {
|
|
||||||
dispatch(mentionCompose(account));
|
|
||||||
},
|
|
||||||
|
|
||||||
onOpenMedia (media, index, lang) {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'MEDIA',
|
|
||||||
modalProps: { media, index, lang },
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onOpenVideo (media, lang, options) {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'VIDEO',
|
|
||||||
modalProps: { media, lang, options },
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onBlock (status) {
|
|
||||||
const account = status.get('account');
|
|
||||||
dispatch(initBlockModal(account));
|
|
||||||
},
|
|
||||||
|
|
||||||
onReport (status) {
|
|
||||||
dispatch(initReport(status.get('account'), status));
|
|
||||||
},
|
|
||||||
|
|
||||||
onMute (account) {
|
|
||||||
dispatch(initMuteModal(account));
|
|
||||||
},
|
|
||||||
|
|
||||||
onMuteConversation (status) {
|
|
||||||
if (status.get('muted')) {
|
|
||||||
dispatch(unmuteStatus(status.get('id')));
|
|
||||||
} else {
|
|
||||||
dispatch(muteStatus(status.get('id')));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onToggleHidden (status) {
|
|
||||||
dispatch(toggleStatusSpoilers(status.get('id')));
|
|
||||||
},
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(DetailedStatus));
|
|
|
@ -69,7 +69,7 @@ import Column from '../ui/components/column';
|
||||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
|
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
|
||||||
|
|
||||||
import ActionBar from './components/action_bar';
|
import ActionBar from './components/action_bar';
|
||||||
import DetailedStatus from './components/detailed_status';
|
import { DetailedStatus } from './components/detailed_status';
|
||||||
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
|
|
@ -99,7 +99,7 @@ export const BlockModal = ({ accountId, acct }) => {
|
||||||
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
|
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Button onClick={handleClick}>
|
<Button onClick={handleClick} autoFocus>
|
||||||
<FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
|
<FormattedMessage id='confirmations.block.confirm' defaultMessage='Block' />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -71,7 +71,10 @@ export const ConfirmationModal: React.FC<
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Button onClick={handleClick}>{confirm}</Button>
|
{/* eslint-disable-next-line jsx-a11y/no-autofocus -- we are in a modal and thus autofocusing is justified */}
|
||||||
|
<Button onClick={handleClick} autoFocus>
|
||||||
|
{confirm}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -88,7 +88,7 @@ export const DomainBlockModal = ({ domain, accountId, acct }) => {
|
||||||
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
|
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Button onClick={handleClick}>
|
<Button onClick={handleClick} autoFocus>
|
||||||
<FormattedMessage id='domain_block_modal.block' defaultMessage='Block server' />
|
<FormattedMessage id='domain_block_modal.block' defaultMessage='Block server' />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,101 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
|
||||||
import api from 'mastodon/api';
|
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
|
||||||
});
|
|
||||||
|
|
||||||
class EmbedModal extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
id: PropTypes.string.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
onError: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
loading: false,
|
|
||||||
oembed: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
const { id } = this.props;
|
|
||||||
|
|
||||||
this.setState({ loading: true });
|
|
||||||
|
|
||||||
api().get(`/api/web/embeds/${id}`).then(res => {
|
|
||||||
this.setState({ loading: false, oembed: res.data });
|
|
||||||
|
|
||||||
const iframeDocument = this.iframe.contentWindow.document;
|
|
||||||
|
|
||||||
iframeDocument.open();
|
|
||||||
iframeDocument.write(res.data.html);
|
|
||||||
iframeDocument.close();
|
|
||||||
|
|
||||||
iframeDocument.body.style.margin = 0;
|
|
||||||
this.iframe.width = iframeDocument.body.scrollWidth;
|
|
||||||
this.iframe.height = iframeDocument.body.scrollHeight;
|
|
||||||
}).catch(error => {
|
|
||||||
this.props.onError(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setIframeRef = c => {
|
|
||||||
this.iframe = c;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleTextareaClick = (e) => {
|
|
||||||
e.target.select();
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { intl, onClose } = this.props;
|
|
||||||
const { oembed } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='modal-root__modal report-modal embed-modal'>
|
|
||||||
<div className='report-modal__target'>
|
|
||||||
<IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} size={16} />
|
|
||||||
<FormattedMessage id='status.embed' defaultMessage='Embed' />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='report-modal__container embed-modal__container' style={{ display: 'block' }}>
|
|
||||||
<p className='hint'>
|
|
||||||
<FormattedMessage id='embed.instructions' defaultMessage='Embed this status on your website by copying the code below.' />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
className='embed-modal__html'
|
|
||||||
readOnly
|
|
||||||
value={oembed && oembed.html || ''}
|
|
||||||
onClick={this.handleTextareaClick}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p className='hint'>
|
|
||||||
<FormattedMessage id='embed.preview' defaultMessage='Here is what it will look like:' />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<iframe
|
|
||||||
className='embed-modal__iframe'
|
|
||||||
frameBorder='0'
|
|
||||||
ref={this.setIframeRef}
|
|
||||||
sandbox='allow-scripts allow-same-origin'
|
|
||||||
title='preview'
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(EmbedModal);
|
|
116
app/javascript/mastodon/features/ui/components/embed_modal.tsx
Normal file
116
app/javascript/mastodon/features/ui/components/embed_modal.tsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import { useRef, useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { showAlertForError } from 'mastodon/actions/alerts';
|
||||||
|
import api from 'mastodon/api';
|
||||||
|
import { Button } from 'mastodon/components/button';
|
||||||
|
import { CopyPasteText } from 'mastodon/components/copy_paste_text';
|
||||||
|
import { useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
interface OEmbedResponse {
|
||||||
|
html: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmbedModal: React.FC<{
|
||||||
|
id: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}> = ({ id, onClose }) => {
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
const intervalRef = useRef<ReturnType<typeof setInterval>>();
|
||||||
|
const [oembed, setOembed] = useState<OEmbedResponse | null>(null);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
api()
|
||||||
|
.get(`/api/web/embeds/${id}`)
|
||||||
|
.then((res) => {
|
||||||
|
const data = res.data as OEmbedResponse;
|
||||||
|
|
||||||
|
setOembed(data);
|
||||||
|
|
||||||
|
const iframeDocument = iframeRef.current?.contentWindow?.document;
|
||||||
|
|
||||||
|
if (!iframeDocument) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
iframeDocument.open();
|
||||||
|
iframeDocument.write(data.html);
|
||||||
|
iframeDocument.close();
|
||||||
|
|
||||||
|
iframeDocument.body.style.margin = '0px';
|
||||||
|
|
||||||
|
// This is our best chance to ensure the parent iframe has the correct height...
|
||||||
|
intervalRef.current = setInterval(
|
||||||
|
() =>
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
if (iframeRef.current) {
|
||||||
|
iframeRef.current.width = `${iframeDocument.body.scrollWidth}px`;
|
||||||
|
iframeRef.current.height = `${iframeDocument.body.scrollHeight}px`;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
})
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
dispatch(showAlertForError(error));
|
||||||
|
});
|
||||||
|
}, [dispatch, id, setOembed]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal dialog-modal'>
|
||||||
|
<div className='dialog-modal__header'>
|
||||||
|
<Button onClick={onClose}>
|
||||||
|
<FormattedMessage id='report.close' defaultMessage='Done' />
|
||||||
|
</Button>
|
||||||
|
<span className='dialog-modal__header__title'>
|
||||||
|
<FormattedMessage id='status.embed' defaultMessage='Get embed code' />
|
||||||
|
</span>
|
||||||
|
<Button secondary onClick={onClose}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='confirmation_modal.cancel'
|
||||||
|
defaultMessage='Cancel'
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='dialog-modal__content'>
|
||||||
|
<div className='dialog-modal__content__form'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='embed.instructions'
|
||||||
|
defaultMessage='Embed this status on your website by copying the code below.'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CopyPasteText value={oembed?.html ?? ''} />
|
||||||
|
|
||||||
|
<FormattedMessage
|
||||||
|
id='embed.preview'
|
||||||
|
defaultMessage='Here is what it will look like:'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<iframe
|
||||||
|
frameBorder='0'
|
||||||
|
ref={iframeRef}
|
||||||
|
sandbox='allow-scripts allow-same-origin'
|
||||||
|
title='Preview'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default EmbedModal;
|
|
@ -137,7 +137,7 @@ export const MuteModal = ({ accountId, acct }) => {
|
||||||
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
|
<FormattedMessage id='confirmation_modal.cancel' defaultMessage='Cancel' />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Button onClick={handleClick}>
|
<Button onClick={handleClick} autoFocus>
|
||||||
<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
|
<FormattedMessage id='confirmations.mute.confirm' defaultMessage='Mute' />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,10 +3,12 @@ import { connect } from 'react-redux';
|
||||||
import { openModal, closeModal } from '../../../actions/modal';
|
import { openModal, closeModal } from '../../../actions/modal';
|
||||||
import ModalRoot from '../components/modal_root';
|
import ModalRoot from '../components/modal_root';
|
||||||
|
|
||||||
|
const defaultProps = {};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
ignoreFocus: state.getIn(['modal', 'ignoreFocus']),
|
ignoreFocus: state.getIn(['modal', 'ignoreFocus']),
|
||||||
type: state.getIn(['modal', 'stack', 0, 'modalType'], null),
|
type: state.getIn(['modal', 'stack', 0, 'modalType'], null),
|
||||||
props: state.getIn(['modal', 'stack', 0, 'modalProps'], {}),
|
props: state.getIn(['modal', 'stack', 0, 'modalProps'], defaultProps),
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
|
@ -4,24 +4,11 @@ import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { NotificationStack } from 'react-notification';
|
import { NotificationStack } from 'react-notification';
|
||||||
|
|
||||||
import { dismissAlert } from '../../../actions/alerts';
|
import { dismissAlert } from 'mastodon/actions/alerts';
|
||||||
import { getAlerts } from '../../../selectors';
|
import { getAlerts } from 'mastodon/selectors';
|
||||||
|
|
||||||
const formatIfNeeded = (intl, message, values) => {
|
|
||||||
if (typeof message === 'object') {
|
|
||||||
return intl.formatMessage(message, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
return message;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { intl }) => ({
|
const mapStateToProps = (state, { intl }) => ({
|
||||||
notifications: getAlerts(state).map(alert => ({
|
notifications: getAlerts(state, { intl }),
|
||||||
...alert,
|
|
||||||
action: formatIfNeeded(intl, alert.action, alert.values),
|
|
||||||
title: formatIfNeeded(intl, alert.title, alert.values),
|
|
||||||
message: formatIfNeeded(intl, alert.message, alert.values),
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch) => ({
|
||||||
|
|
|
@ -319,8 +319,8 @@ class UI extends PureComponent {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
e.dataTransfer.dropEffect = 'copy';
|
e.dataTransfer.dropEffect = 'copy';
|
||||||
} catch (err) {
|
} catch {
|
||||||
|
// do nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -308,7 +308,6 @@
|
||||||
"lists.search": "Buscar entre la chent a la quala sigues",
|
"lists.search": "Buscar entre la chent a la quala sigues",
|
||||||
"lists.subheading": "Las tuyas listas",
|
"lists.subheading": "Las tuyas listas",
|
||||||
"load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
|
"load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Amaga la imachen} other {Amaga las imáchens}}",
|
|
||||||
"moved_to_account_banner.text": "La tuya cuenta {disabledAccount} ye actualment deshabilitada perque t'has mudau a {movedToAccount}.",
|
"moved_to_account_banner.text": "La tuya cuenta {disabledAccount} ye actualment deshabilitada perque t'has mudau a {movedToAccount}.",
|
||||||
"navigation_bar.about": "Sobre",
|
"navigation_bar.about": "Sobre",
|
||||||
"navigation_bar.blocks": "Usuarios blocaus",
|
"navigation_bar.blocks": "Usuarios blocaus",
|
||||||
|
|
|
@ -95,6 +95,8 @@
|
||||||
"block_modal.title": "أتريد حظر هذا المستخدم؟",
|
"block_modal.title": "أتريد حظر هذا المستخدم؟",
|
||||||
"block_modal.you_wont_see_mentions": "لن تر المنشورات التي يُشار فيهم إليه.",
|
"block_modal.you_wont_see_mentions": "لن تر المنشورات التي يُشار فيهم إليه.",
|
||||||
"boost_modal.combo": "يُمكنك الضّغط على {combo} لتخطي هذا في المرة المُقبلة",
|
"boost_modal.combo": "يُمكنك الضّغط على {combo} لتخطي هذا في المرة المُقبلة",
|
||||||
|
"boost_modal.reblog": "أتريد إعادة نشر المنشور؟",
|
||||||
|
"boost_modal.undo_reblog": "أتريد إلغاء إعادة نشر المنشور؟",
|
||||||
"bundle_column_error.copy_stacktrace": "انسخ تقرير الخطأ",
|
"bundle_column_error.copy_stacktrace": "انسخ تقرير الخطأ",
|
||||||
"bundle_column_error.error.body": "لا يمكن تقديم الصفحة المطلوبة. قد يكون بسبب خطأ في التعليمات البرمجية، أو مشكلة توافق المتصفح.",
|
"bundle_column_error.error.body": "لا يمكن تقديم الصفحة المطلوبة. قد يكون بسبب خطأ في التعليمات البرمجية، أو مشكلة توافق المتصفح.",
|
||||||
"bundle_column_error.error.title": "أوه لا!",
|
"bundle_column_error.error.title": "أوه لا!",
|
||||||
|
@ -227,10 +229,10 @@
|
||||||
"domain_pill.their_username": "مُعرّفُهم الفريد على الخادم. من الممكن العثور على مستخدمين بنفس اسم المستخدم على خوادم مختلفة.",
|
"domain_pill.their_username": "مُعرّفُهم الفريد على الخادم. من الممكن العثور على مستخدمين بنفس اسم المستخدم على خوادم مختلفة.",
|
||||||
"domain_pill.username": "اسم المستخدم",
|
"domain_pill.username": "اسم المستخدم",
|
||||||
"domain_pill.whats_in_a_handle": "ما المقصود بالمُعرِّف؟",
|
"domain_pill.whats_in_a_handle": "ما المقصود بالمُعرِّف؟",
|
||||||
"domain_pill.who_they_are": "بما أن المعرفات تقول من هو الشخص ومكان وجوده، يمكنك التفاعل مع الناس عبر الويب الاجتماعي لل <button>منصات التي تعمل ب ActivityPub</button>.",
|
"domain_pill.who_they_are": "بما أن المُعرّفات تحدد هوية الشخص ومكان وجوده، فبإمكانك التفاعل مع الأشخاص عبر الويب الاجتماعي لـ <button>المنصات التي تعمل بواسطة أكتيفيتي بوب</button>.",
|
||||||
"domain_pill.who_you_are": "بما أن معرفك يقول من أنت ومكان وجوده، يمكن للناس التفاعل معك عبر الويب الاجتماعي لل <button>منصات التي تعمل ب ActivityPub</button>.",
|
"domain_pill.who_you_are": "بما أن مُعرّفك يحدد هويتك ومكان وجوده، فبإمكانك الآخرين التفاعل معك عبر الويب الاجتماعي لـ <button>المنصات التي تعمل بواسطة أكتيفيتي بوب</button>.",
|
||||||
"domain_pill.your_handle": "عنوانك الكامل:",
|
"domain_pill.your_handle": "عنوانك الكامل:",
|
||||||
"domain_pill.your_server": "منزلك الرقمي، حيث تعيش جميع مشاركاتك. لا تحب هذا؟ إنقل الخوادم في أي وقت واخضر متابعينك أيضًا.",
|
"domain_pill.your_server": "موطِنك الرقمي، حيث توجد فيه كافة منشوراتك. ألا يعجبك المكان؟ يمكنك الانتقال بين الخوادم في أي وقت واصطحاب متابعيك أيضاً.",
|
||||||
"domain_pill.your_username": "معرفك الفريد على هذا الخادم. من الممكن العثور على مستخدمين بنفس إسم المستخدم على خوادم مختلفة.",
|
"domain_pill.your_username": "معرفك الفريد على هذا الخادم. من الممكن العثور على مستخدمين بنفس إسم المستخدم على خوادم مختلفة.",
|
||||||
"embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.",
|
"embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.",
|
||||||
"embed.preview": "إليك ما سيبدو عليه:",
|
"embed.preview": "إليك ما سيبدو عليه:",
|
||||||
|
@ -290,7 +292,7 @@
|
||||||
"filter_modal.added.review_and_configure": "لمراجعة وزيادة تكوين فئة عوامل التصفية هذه، انتقل إلى {settings_link}.",
|
"filter_modal.added.review_and_configure": "لمراجعة وزيادة تكوين فئة عوامل التصفية هذه، انتقل إلى {settings_link}.",
|
||||||
"filter_modal.added.review_and_configure_title": "إعدادات التصفية",
|
"filter_modal.added.review_and_configure_title": "إعدادات التصفية",
|
||||||
"filter_modal.added.settings_link": "صفحة الإعدادات",
|
"filter_modal.added.settings_link": "صفحة الإعدادات",
|
||||||
"filter_modal.added.short_explanation": "تمت إضافة هذه المشاركة إلى فئة الفلاتر التالية: {title}.",
|
"filter_modal.added.short_explanation": "تمت إضافة هذا المنشور إلى فئة عوامل التصفية التالية: {title}.",
|
||||||
"filter_modal.added.title": "تمت إضافة عامل التصفية!",
|
"filter_modal.added.title": "تمت إضافة عامل التصفية!",
|
||||||
"filter_modal.select_filter.context_mismatch": "لا ينطبق على هذا السياق",
|
"filter_modal.select_filter.context_mismatch": "لا ينطبق على هذا السياق",
|
||||||
"filter_modal.select_filter.expired": "منتهية الصلاحيّة",
|
"filter_modal.select_filter.expired": "منتهية الصلاحيّة",
|
||||||
|
@ -348,6 +350,10 @@
|
||||||
"hashtag.follow": "اتبع الوسم",
|
"hashtag.follow": "اتبع الوسم",
|
||||||
"hashtag.unfollow": "ألغِ متابعة الوسم",
|
"hashtag.unfollow": "ألغِ متابعة الوسم",
|
||||||
"hashtags.and_other": "…و {count, plural, zero {} one {# واحد آخر} two {# اثنان آخران} few {# آخرون} many {# آخَرًا}other {# آخرون}}",
|
"hashtags.and_other": "…و {count, plural, zero {} one {# واحد آخر} two {# اثنان آخران} few {# آخرون} many {# آخَرًا}other {# آخرون}}",
|
||||||
|
"hints.profiles.see_more_followers": "عرض المزيد من المتابعين على {domain}",
|
||||||
|
"hints.profiles.see_more_posts": "عرض المزيد من المنشورات من {domain}",
|
||||||
|
"hints.threads.replies_may_be_missing": "قد تكون الردود الواردة من الخوادم الأخرى غائبة.",
|
||||||
|
"hints.threads.see_more": "اطلع على المزيد من الردود على {domain}",
|
||||||
"home.column_settings.show_reblogs": "اعرض المعاد نشرها",
|
"home.column_settings.show_reblogs": "اعرض المعاد نشرها",
|
||||||
"home.column_settings.show_replies": "اعرض الردود",
|
"home.column_settings.show_replies": "اعرض الردود",
|
||||||
"home.hide_announcements": "إخفاء الإعلانات",
|
"home.hide_announcements": "إخفاء الإعلانات",
|
||||||
|
@ -356,8 +362,10 @@
|
||||||
"home.pending_critical_update.title": "تحديث أمان حرج متوفر!",
|
"home.pending_critical_update.title": "تحديث أمان حرج متوفر!",
|
||||||
"home.show_announcements": "إظهار الإعلانات",
|
"home.show_announcements": "إظهار الإعلانات",
|
||||||
"ignore_notifications_modal.disclaimer": "لا يمكن لـ Mastodon إبلاغ المستخدمين بأنك قد تجاهلت إشعاراتهم. تجاهل الإشعارات لن يمنع إرسال الرسائل نفسها.",
|
"ignore_notifications_modal.disclaimer": "لا يمكن لـ Mastodon إبلاغ المستخدمين بأنك قد تجاهلت إشعاراتهم. تجاهل الإشعارات لن يمنع إرسال الرسائل نفسها.",
|
||||||
|
"ignore_notifications_modal.filter_instead": "تصفيتها بدلا من ذلك",
|
||||||
"ignore_notifications_modal.ignore": "تجاهل الإشعارات",
|
"ignore_notifications_modal.ignore": "تجاهل الإشعارات",
|
||||||
"ignore_notifications_modal.limited_accounts_title": "تجاهل الإشعارات من الحسابات التي هي تحت الإشراف؟",
|
"ignore_notifications_modal.limited_accounts_title": "تجاهل الإشعارات من الحسابات التي هي تحت الإشراف؟",
|
||||||
|
"ignore_notifications_modal.new_accounts_title": "تجاهل الإشعارات الصادرة من الحسابات الجديدة؟",
|
||||||
"interaction_modal.description.favourite": "بفضل حساب على ماستدون، يمكنك إضافة هذا المنشور إلى مفضلتك لإبلاغ الناشر عن تقديرك وكذا للاحتفاظ بالمنشور إلى وقت لاحق.",
|
"interaction_modal.description.favourite": "بفضل حساب على ماستدون، يمكنك إضافة هذا المنشور إلى مفضلتك لإبلاغ الناشر عن تقديرك وكذا للاحتفاظ بالمنشور إلى وقت لاحق.",
|
||||||
"interaction_modal.description.follow": "بفضل حساب في ماستدون، يمكنك متابعة {name} وتلقي منشوراته في موجزات خيطك الرئيس.",
|
"interaction_modal.description.follow": "بفضل حساب في ماستدون، يمكنك متابعة {name} وتلقي منشوراته في موجزات خيطك الرئيس.",
|
||||||
"interaction_modal.description.reblog": "مع حساب في ماستدون، يمكنك تعزيز هذا المنشور ومشاركته مع مُتابِعيك.",
|
"interaction_modal.description.reblog": "مع حساب في ماستدون، يمكنك تعزيز هذا المنشور ومشاركته مع مُتابِعيك.",
|
||||||
|
@ -435,7 +443,6 @@
|
||||||
"lists.subheading": "قوائمك",
|
"lists.subheading": "قوائمك",
|
||||||
"load_pending": "{count, plural, one {# عنصر جديد} other {# عناصر جديدة}}",
|
"load_pending": "{count, plural, one {# عنصر جديد} other {# عناصر جديدة}}",
|
||||||
"loading_indicator.label": "جاري التحميل…",
|
"loading_indicator.label": "جاري التحميل…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, zero {} one {اخف الصورة} two {اخف الصورتين} few {اخف الصور} many {اخف الصور} other {اخف الصور}}",
|
|
||||||
"moved_to_account_banner.text": "حسابك {disabledAccount} معطل حاليًا لأنك انتقلت إلى {movedToAccount}.",
|
"moved_to_account_banner.text": "حسابك {disabledAccount} معطل حاليًا لأنك انتقلت إلى {movedToAccount}.",
|
||||||
"mute_modal.hide_from_notifications": "إخفاء من قائمة الإشعارات",
|
"mute_modal.hide_from_notifications": "إخفاء من قائمة الإشعارات",
|
||||||
"mute_modal.hide_options": "إخفاء الخيارات",
|
"mute_modal.hide_options": "إخفاء الخيارات",
|
||||||
|
@ -447,6 +454,7 @@
|
||||||
"mute_modal.you_wont_see_mentions": "لن تر المنشورات التي يُشار فيها إليه.",
|
"mute_modal.you_wont_see_mentions": "لن تر المنشورات التي يُشار فيها إليه.",
|
||||||
"mute_modal.you_wont_see_posts": "سيكون بإمكانه رؤية منشوراتك، لكنك لن ترى منشوراته.",
|
"mute_modal.you_wont_see_posts": "سيكون بإمكانه رؤية منشوراتك، لكنك لن ترى منشوراته.",
|
||||||
"navigation_bar.about": "عن",
|
"navigation_bar.about": "عن",
|
||||||
|
"navigation_bar.administration": "الإدارة",
|
||||||
"navigation_bar.advanced_interface": "افتحه في واجهة الويب المتقدمة",
|
"navigation_bar.advanced_interface": "افتحه في واجهة الويب المتقدمة",
|
||||||
"navigation_bar.blocks": "الحسابات المحجوبة",
|
"navigation_bar.blocks": "الحسابات المحجوبة",
|
||||||
"navigation_bar.bookmarks": "الفواصل المرجعية",
|
"navigation_bar.bookmarks": "الفواصل المرجعية",
|
||||||
|
@ -463,6 +471,7 @@
|
||||||
"navigation_bar.follows_and_followers": "المتابِعون والمتابَعون",
|
"navigation_bar.follows_and_followers": "المتابِعون والمتابَعون",
|
||||||
"navigation_bar.lists": "القوائم",
|
"navigation_bar.lists": "القوائم",
|
||||||
"navigation_bar.logout": "خروج",
|
"navigation_bar.logout": "خروج",
|
||||||
|
"navigation_bar.moderation": "الإشراف",
|
||||||
"navigation_bar.mutes": "الحسابات المكتومة",
|
"navigation_bar.mutes": "الحسابات المكتومة",
|
||||||
"navigation_bar.opened_in_classic_interface": "تُفتَح المنشورات والحسابات وغيرها من الصفحات الخاصة بشكل مبدئي على واجهة الويب التقليدية.",
|
"navigation_bar.opened_in_classic_interface": "تُفتَح المنشورات والحسابات وغيرها من الصفحات الخاصة بشكل مبدئي على واجهة الويب التقليدية.",
|
||||||
"navigation_bar.personal": "شخصي",
|
"navigation_bar.personal": "شخصي",
|
||||||
|
@ -484,7 +493,7 @@
|
||||||
"notification.mention": "إشارة",
|
"notification.mention": "إشارة",
|
||||||
"notification.moderation-warning.learn_more": "اعرف المزيد",
|
"notification.moderation-warning.learn_more": "اعرف المزيد",
|
||||||
"notification.moderation_warning": "لقد تلقيت تحذيرًا بالإشراف",
|
"notification.moderation_warning": "لقد تلقيت تحذيرًا بالإشراف",
|
||||||
"notification.moderation_warning.action_delete_statuses": "تم إزالة بعض مشاركاتك.",
|
"notification.moderation_warning.action_delete_statuses": "تم حذف بعض من منشوراتك.",
|
||||||
"notification.moderation_warning.action_disable": "تم تعطيل حسابك.",
|
"notification.moderation_warning.action_disable": "تم تعطيل حسابك.",
|
||||||
"notification.moderation_warning.action_mark_statuses_as_sensitive": "بعض من منشوراتك تم تصنيفها على أنها حساسة.",
|
"notification.moderation_warning.action_mark_statuses_as_sensitive": "بعض من منشوراتك تم تصنيفها على أنها حساسة.",
|
||||||
"notification.moderation_warning.action_none": "لقد تلقى حسابك تحذيرا بالإشراف.",
|
"notification.moderation_warning.action_none": "لقد تلقى حسابك تحذيرا بالإشراف.",
|
||||||
|
@ -502,12 +511,16 @@
|
||||||
"notification.status": "{name} نشر للتو",
|
"notification.status": "{name} نشر للتو",
|
||||||
"notification.update": "عدّلَ {name} منشورًا",
|
"notification.update": "عدّلَ {name} منشورًا",
|
||||||
"notification_requests.accept": "موافقة",
|
"notification_requests.accept": "موافقة",
|
||||||
|
"notification_requests.confirm_accept_multiple.title": "قبول طلبات الإشعار؟",
|
||||||
|
"notification_requests.confirm_dismiss_multiple.title": "تجاهل طلبات الإشعار؟",
|
||||||
"notification_requests.dismiss": "تخطي",
|
"notification_requests.dismiss": "تخطي",
|
||||||
"notification_requests.edit_selection": "تعديل",
|
"notification_requests.edit_selection": "تعديل",
|
||||||
"notification_requests.exit_selection": "تمّ",
|
"notification_requests.exit_selection": "تمّ",
|
||||||
"notification_requests.explainer_for_limited_account": "تم تصفية الإشعارات من هذا الحساب لأن الحساب تم تقييده من قبل مشرف.",
|
"notification_requests.explainer_for_limited_account": "تم تصفية الإشعارات من هذا الحساب لأن الحساب تم تقييده من قبل مشرف.",
|
||||||
|
"notification_requests.minimize_banner": "تصغير شريط الإشعارات المُصفاة",
|
||||||
"notification_requests.notifications_from": "إشعارات من {name}",
|
"notification_requests.notifications_from": "إشعارات من {name}",
|
||||||
"notification_requests.title": "الإشعارات المصفاة",
|
"notification_requests.title": "الإشعارات المصفاة",
|
||||||
|
"notification_requests.view": "عرض الإشعارات",
|
||||||
"notifications.clear": "مسح الإشعارات",
|
"notifications.clear": "مسح الإشعارات",
|
||||||
"notifications.clear_confirmation": "متأكد من أنك تود مسح جميع الإشعارات الخاصة بك و المتلقاة إلى حد الآن ؟",
|
"notifications.clear_confirmation": "متأكد من أنك تود مسح جميع الإشعارات الخاصة بك و المتلقاة إلى حد الآن ؟",
|
||||||
"notifications.clear_title": "أترغب في مسح الإشعارات؟",
|
"notifications.clear_title": "أترغب في مسح الإشعارات؟",
|
||||||
|
@ -520,7 +533,7 @@
|
||||||
"notifications.column_settings.filter_bar.advanced": "عرض جميع الفئات",
|
"notifications.column_settings.filter_bar.advanced": "عرض جميع الفئات",
|
||||||
"notifications.column_settings.filter_bar.category": "شريط التصفية السريعة",
|
"notifications.column_settings.filter_bar.category": "شريط التصفية السريعة",
|
||||||
"notifications.column_settings.follow": "متابعُون جُدُد:",
|
"notifications.column_settings.follow": "متابعُون جُدُد:",
|
||||||
"notifications.column_settings.follow_request": "الطلبات الجديد لِمتابَعتك:",
|
"notifications.column_settings.follow_request": "الطلبات الجديدة لِمتابَعتك:",
|
||||||
"notifications.column_settings.mention": "الإشارات:",
|
"notifications.column_settings.mention": "الإشارات:",
|
||||||
"notifications.column_settings.poll": "نتائج استطلاع الرأي:",
|
"notifications.column_settings.poll": "نتائج استطلاع الرأي:",
|
||||||
"notifications.column_settings.push": "الإشعارات",
|
"notifications.column_settings.push": "الإشعارات",
|
||||||
|
@ -747,7 +760,7 @@
|
||||||
"status.history.edited": "عدله {name} {date}",
|
"status.history.edited": "عدله {name} {date}",
|
||||||
"status.load_more": "حمّل المزيد",
|
"status.load_more": "حمّل المزيد",
|
||||||
"status.media.open": "اضغط للفتح",
|
"status.media.open": "اضغط للفتح",
|
||||||
"status.media.show": "اضغط للإظهار",
|
"status.media.show": "اضغط لإظهاره",
|
||||||
"status.media_hidden": "وسائط مخفية",
|
"status.media_hidden": "وسائط مخفية",
|
||||||
"status.mention": "أذكُر @{name}",
|
"status.mention": "أذكُر @{name}",
|
||||||
"status.more": "المزيد",
|
"status.more": "المزيد",
|
||||||
|
|
|
@ -268,7 +268,6 @@
|
||||||
"lists.search": "Buscar ente los perfiles que sigues",
|
"lists.search": "Buscar ente los perfiles que sigues",
|
||||||
"lists.subheading": "Les tos llistes",
|
"lists.subheading": "Les tos llistes",
|
||||||
"load_pending": "{count, plural, one {# elementu nuevu} other {# elementos nuevos}}",
|
"load_pending": "{count, plural, one {# elementu nuevu} other {# elementos nuevos}}",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Anubrir la imaxe} other {Anubrir les imáxenes}}",
|
|
||||||
"navigation_bar.about": "Tocante a",
|
"navigation_bar.about": "Tocante a",
|
||||||
"navigation_bar.blocks": "Perfiles bloquiaos",
|
"navigation_bar.blocks": "Perfiles bloquiaos",
|
||||||
"navigation_bar.bookmarks": "Marcadores",
|
"navigation_bar.bookmarks": "Marcadores",
|
||||||
|
|
|
@ -437,7 +437,6 @@
|
||||||
"lists.subheading": "Вашыя спісы",
|
"lists.subheading": "Вашыя спісы",
|
||||||
"load_pending": "{count, plural, one {# новы элемент} few {# новыя элементы} many {# новых элементаў} other {# новых элементаў}}",
|
"load_pending": "{count, plural, one {# новы элемент} few {# новыя элементы} many {# новых элементаў} other {# новых элементаў}}",
|
||||||
"loading_indicator.label": "Загрузка…",
|
"loading_indicator.label": "Загрузка…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Схаваць відарыс} other {Схаваць відарысы}}",
|
|
||||||
"moved_to_account_banner.text": "Ваш уліковы запіс {disabledAccount} зараз адключаны таму што вы перанесены на {movedToAccount}.",
|
"moved_to_account_banner.text": "Ваш уліковы запіс {disabledAccount} зараз адключаны таму што вы перанесены на {movedToAccount}.",
|
||||||
"mute_modal.hide_from_notifications": "Схаваць з апавяшчэнняў",
|
"mute_modal.hide_from_notifications": "Схаваць з апавяшчэнняў",
|
||||||
"mute_modal.hide_options": "Схаваць опцыі",
|
"mute_modal.hide_options": "Схаваць опцыі",
|
||||||
|
|
|
@ -444,7 +444,6 @@
|
||||||
"lists.subheading": "Вашите списъци",
|
"lists.subheading": "Вашите списъци",
|
||||||
"load_pending": "{count, plural, one {# нов елемент} other {# нови елемента}}",
|
"load_pending": "{count, plural, one {# нов елемент} other {# нови елемента}}",
|
||||||
"loading_indicator.label": "Зареждане…",
|
"loading_indicator.label": "Зареждане…",
|
||||||
"media_gallery.toggle_visible": "Скриване на {number, plural, one {изображение} other {изображения}}",
|
|
||||||
"moved_to_account_banner.text": "Вашият акаунт {disabledAccount} сега е изключен, защото се преместихте в {movedToAccount}.",
|
"moved_to_account_banner.text": "Вашият акаунт {disabledAccount} сега е изключен, защото се преместихте в {movedToAccount}.",
|
||||||
"mute_modal.hide_from_notifications": "Скриване от известията",
|
"mute_modal.hide_from_notifications": "Скриване от известията",
|
||||||
"mute_modal.hide_options": "Скриване на възможностите",
|
"mute_modal.hide_options": "Скриване на възможностите",
|
||||||
|
|
|
@ -288,7 +288,6 @@
|
||||||
"lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন",
|
"lists.search": "যাদের অনুসরণ করেন তাদের ভেতরে খুঁজুন",
|
||||||
"lists.subheading": "আপনার তালিকা",
|
"lists.subheading": "আপনার তালিকা",
|
||||||
"load_pending": "{count, plural, one {# নতুন জিনিস} other {# নতুন জিনিস}}",
|
"load_pending": "{count, plural, one {# নতুন জিনিস} other {# নতুন জিনিস}}",
|
||||||
"media_gallery.toggle_visible": "দৃশ্যতার অবস্থা বদলান",
|
|
||||||
"navigation_bar.about": "পরিচিতি",
|
"navigation_bar.about": "পরিচিতি",
|
||||||
"navigation_bar.blocks": "বন্ধ করা ব্যবহারকারী",
|
"navigation_bar.blocks": "বন্ধ করা ব্যবহারকারী",
|
||||||
"navigation_bar.bookmarks": "বুকমার্ক",
|
"navigation_bar.bookmarks": "বুকমার্ক",
|
||||||
|
|
|
@ -360,7 +360,6 @@
|
||||||
"lists.subheading": "Ho listennoù",
|
"lists.subheading": "Ho listennoù",
|
||||||
"load_pending": "{count, plural, one {# dra nevez} other {# dra nevez}}",
|
"load_pending": "{count, plural, one {# dra nevez} other {# dra nevez}}",
|
||||||
"loading_indicator.label": "O kargañ…",
|
"loading_indicator.label": "O kargañ…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Kuzhat ar skeudenn} other {Kuzhat ar skeudenn}}",
|
|
||||||
"navigation_bar.about": "Diwar-benn",
|
"navigation_bar.about": "Diwar-benn",
|
||||||
"navigation_bar.blocks": "Implijer·ezed·ien berzet",
|
"navigation_bar.blocks": "Implijer·ezed·ien berzet",
|
||||||
"navigation_bar.bookmarks": "Sinedoù",
|
"navigation_bar.bookmarks": "Sinedoù",
|
||||||
|
|
|
@ -457,7 +457,7 @@
|
||||||
"lists.subheading": "Les teves llistes",
|
"lists.subheading": "Les teves llistes",
|
||||||
"load_pending": "{count, plural, one {# element nou} other {# elements nous}}",
|
"load_pending": "{count, plural, one {# element nou} other {# elements nous}}",
|
||||||
"loading_indicator.label": "Es carrega…",
|
"loading_indicator.label": "Es carrega…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Amaga la imatge} other {Amaga les imatges}}",
|
"media_gallery.hide": "Amaga",
|
||||||
"moved_to_account_banner.text": "El teu compte {disabledAccount} està desactivat perquè l'has mogut a {movedToAccount}.",
|
"moved_to_account_banner.text": "El teu compte {disabledAccount} està desactivat perquè l'has mogut a {movedToAccount}.",
|
||||||
"mute_modal.hide_from_notifications": "Amaga de les notificacions",
|
"mute_modal.hide_from_notifications": "Amaga de les notificacions",
|
||||||
"mute_modal.hide_options": "Amaga les opcions",
|
"mute_modal.hide_options": "Amaga les opcions",
|
||||||
|
@ -780,6 +780,7 @@
|
||||||
"status.bookmark": "Marca",
|
"status.bookmark": "Marca",
|
||||||
"status.cancel_reblog_private": "Desfés l'impuls",
|
"status.cancel_reblog_private": "Desfés l'impuls",
|
||||||
"status.cannot_reblog": "No es pot impulsar aquest tut",
|
"status.cannot_reblog": "No es pot impulsar aquest tut",
|
||||||
|
"status.continued_thread": "Continuació del fil",
|
||||||
"status.copy": "Copia l'enllaç al tut",
|
"status.copy": "Copia l'enllaç al tut",
|
||||||
"status.delete": "Elimina",
|
"status.delete": "Elimina",
|
||||||
"status.detailed_status": "Vista detallada de la conversa",
|
"status.detailed_status": "Vista detallada de la conversa",
|
||||||
|
@ -813,6 +814,7 @@
|
||||||
"status.reblogs.empty": "Encara no ha impulsat ningú aquest tut. Quan algú ho faci, apareixerà aquí.",
|
"status.reblogs.empty": "Encara no ha impulsat ningú aquest tut. Quan algú ho faci, apareixerà aquí.",
|
||||||
"status.redraft": "Esborra i reescriu",
|
"status.redraft": "Esborra i reescriu",
|
||||||
"status.remove_bookmark": "Elimina el marcador",
|
"status.remove_bookmark": "Elimina el marcador",
|
||||||
|
"status.replied_in_thread": "Respost al fil",
|
||||||
"status.replied_to": "En resposta a {name}",
|
"status.replied_to": "En resposta a {name}",
|
||||||
"status.reply": "Respon",
|
"status.reply": "Respon",
|
||||||
"status.replyAll": "Respon al fil",
|
"status.replyAll": "Respon al fil",
|
||||||
|
|
|
@ -355,7 +355,6 @@
|
||||||
"lists.search": "بگەڕێ لەناو ئەو کەسانەی کە شوێنیان کەوتویت",
|
"lists.search": "بگەڕێ لەناو ئەو کەسانەی کە شوێنیان کەوتویت",
|
||||||
"lists.subheading": "لیستەکانت",
|
"lists.subheading": "لیستەکانت",
|
||||||
"load_pending": "{count, plural, one {# بەڕگەی نوێ} other {# بەڕگەی نوێ}}",
|
"load_pending": "{count, plural, one {# بەڕگەی نوێ} other {# بەڕگەی نوێ}}",
|
||||||
"media_gallery.toggle_visible": "شاردنەوەی {number, plural, one {image} other {images}}",
|
|
||||||
"moved_to_account_banner.text": "ئەکاونتەکەت {disabledAccount} لە ئێستادا لەکارخراوە چونکە تۆ چوویتە {movedToAccount}.",
|
"moved_to_account_banner.text": "ئەکاونتەکەت {disabledAccount} لە ئێستادا لەکارخراوە چونکە تۆ چوویتە {movedToAccount}.",
|
||||||
"navigation_bar.about": "دەربارە",
|
"navigation_bar.about": "دەربارە",
|
||||||
"navigation_bar.blocks": "بەکارهێنەرە بلۆککراوەکان",
|
"navigation_bar.blocks": "بەکارهێنەرە بلۆککراوەکان",
|
||||||
|
|
|
@ -214,7 +214,6 @@
|
||||||
"lists.search": "Circà indè i vostr'abbunamenti",
|
"lists.search": "Circà indè i vostr'abbunamenti",
|
||||||
"lists.subheading": "E vo liste",
|
"lists.subheading": "E vo liste",
|
||||||
"load_pending": "{count, plural, one {# entrata nova} other {# entrate nove}}",
|
"load_pending": "{count, plural, one {# entrata nova} other {# entrate nove}}",
|
||||||
"media_gallery.toggle_visible": "Piattà {number, plural, one {ritrattu} other {ritratti}}",
|
|
||||||
"navigation_bar.blocks": "Utilizatori bluccati",
|
"navigation_bar.blocks": "Utilizatori bluccati",
|
||||||
"navigation_bar.bookmarks": "Segnalibri",
|
"navigation_bar.bookmarks": "Segnalibri",
|
||||||
"navigation_bar.community_timeline": "Linea pubblica lucale",
|
"navigation_bar.community_timeline": "Linea pubblica lucale",
|
||||||
|
|
|
@ -435,7 +435,6 @@
|
||||||
"lists.subheading": "Vaše seznamy",
|
"lists.subheading": "Vaše seznamy",
|
||||||
"load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položek} other {# nových položek}}",
|
"load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položek} other {# nových položek}}",
|
||||||
"loading_indicator.label": "Načítání…",
|
"loading_indicator.label": "Načítání…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Skrýt obrázek} few {Skrýt obrázky} many {Skrýt obrázky} other {Skrýt obrázky}}",
|
|
||||||
"moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.",
|
"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 notifikací",
|
||||||
"mute_modal.hide_options": "Skrýt možnosti",
|
"mute_modal.hide_options": "Skrýt možnosti",
|
||||||
|
|
|
@ -97,7 +97,7 @@
|
||||||
"block_modal.title": "Blocio defnyddiwr?",
|
"block_modal.title": "Blocio defnyddiwr?",
|
||||||
"block_modal.you_wont_see_mentions": "Fyddwch chi ddim yn gweld postiadau sy'n sôn amdanyn nhw.",
|
"block_modal.you_wont_see_mentions": "Fyddwch chi ddim yn gweld postiadau sy'n sôn amdanyn nhw.",
|
||||||
"boost_modal.combo": "Mae modd pwyso {combo} er mwyn hepgor hyn tro nesa",
|
"boost_modal.combo": "Mae modd pwyso {combo} er mwyn hepgor hyn tro nesa",
|
||||||
"boost_modal.reblog": "Hybu postiad",
|
"boost_modal.reblog": "Hybu postiad?",
|
||||||
"boost_modal.undo_reblog": "Dad-hybu postiad?",
|
"boost_modal.undo_reblog": "Dad-hybu postiad?",
|
||||||
"bundle_column_error.copy_stacktrace": "Copïo'r adroddiad gwall",
|
"bundle_column_error.copy_stacktrace": "Copïo'r adroddiad gwall",
|
||||||
"bundle_column_error.error.body": "Nid oedd modd cynhyrchu'r dudalen honno. Gall fod oherwydd gwall yn ein cod neu fater cydnawsedd porwr.",
|
"bundle_column_error.error.body": "Nid oedd modd cynhyrchu'r dudalen honno. Gall fod oherwydd gwall yn ein cod neu fater cydnawsedd porwr.",
|
||||||
|
@ -457,7 +457,7 @@
|
||||||
"lists.subheading": "Eich rhestrau",
|
"lists.subheading": "Eich rhestrau",
|
||||||
"load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}",
|
"load_pending": "{count, plural, one {# eitem newydd} other {# eitem newydd}}",
|
||||||
"loading_indicator.label": "Yn llwytho…",
|
"loading_indicator.label": "Yn llwytho…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Cuddio delwedd} other {Cuddio delwedd}}",
|
"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": "Ar hyn y bryd, mae eich cyfrif {disabledAccount} wedi ei analluogi am i chi symud i {movedToAccount}.",
|
||||||
"mute_modal.hide_from_notifications": "Cuddio rhag hysbysiadau",
|
"mute_modal.hide_from_notifications": "Cuddio rhag hysbysiadau",
|
||||||
"mute_modal.hide_options": "Cuddio'r dewis",
|
"mute_modal.hide_options": "Cuddio'r dewis",
|
||||||
|
@ -780,6 +780,7 @@
|
||||||
"status.bookmark": "Llyfrnodi",
|
"status.bookmark": "Llyfrnodi",
|
||||||
"status.cancel_reblog_private": "Dadhybu",
|
"status.cancel_reblog_private": "Dadhybu",
|
||||||
"status.cannot_reblog": "Nid oes modd hybu'r postiad hwn",
|
"status.cannot_reblog": "Nid oes modd hybu'r postiad hwn",
|
||||||
|
"status.continued_thread": "Edefyn parhaus",
|
||||||
"status.copy": "Copïo dolen i'r post",
|
"status.copy": "Copïo dolen i'r post",
|
||||||
"status.delete": "Dileu",
|
"status.delete": "Dileu",
|
||||||
"status.detailed_status": "Golwg manwl o'r sgwrs",
|
"status.detailed_status": "Golwg manwl o'r sgwrs",
|
||||||
|
@ -813,6 +814,7 @@
|
||||||
"status.reblogs.empty": "Does neb wedi hybio'r post yma eto. Pan y bydd rhywun yn gwneud, byddent yn ymddangos yma.",
|
"status.reblogs.empty": "Does neb wedi hybio'r post yma eto. Pan y bydd rhywun yn gwneud, byddent yn ymddangos yma.",
|
||||||
"status.redraft": "Dileu ac ailddrafftio",
|
"status.redraft": "Dileu ac ailddrafftio",
|
||||||
"status.remove_bookmark": "Tynnu nod tudalen",
|
"status.remove_bookmark": "Tynnu nod tudalen",
|
||||||
|
"status.replied_in_thread": "Atebodd mewn edefyn",
|
||||||
"status.replied_to": "Wedi ateb {name}",
|
"status.replied_to": "Wedi ateb {name}",
|
||||||
"status.reply": "Ateb",
|
"status.reply": "Ateb",
|
||||||
"status.replyAll": "Ateb i edefyn",
|
"status.replyAll": "Ateb i edefyn",
|
||||||
|
|
|
@ -457,7 +457,7 @@
|
||||||
"lists.subheading": "Dine lister",
|
"lists.subheading": "Dine lister",
|
||||||
"load_pending": "{count, plural, one {# nyt emne} other {# nye emner}}",
|
"load_pending": "{count, plural, one {# nyt emne} other {# nye emner}}",
|
||||||
"loading_indicator.label": "Indlæser…",
|
"loading_indicator.label": "Indlæser…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Skjul billede} other {Skjul billeder}}",
|
"media_gallery.hide": "Skjul",
|
||||||
"moved_to_account_banner.text": "Din konto {disabledAccount} er pt. deaktiveret, da du flyttede til {movedToAccount}.",
|
"moved_to_account_banner.text": "Din konto {disabledAccount} er pt. deaktiveret, da du flyttede til {movedToAccount}.",
|
||||||
"mute_modal.hide_from_notifications": "Skjul fra notifikationer",
|
"mute_modal.hide_from_notifications": "Skjul fra notifikationer",
|
||||||
"mute_modal.hide_options": "Skjul valgmuligheder",
|
"mute_modal.hide_options": "Skjul valgmuligheder",
|
||||||
|
@ -780,6 +780,7 @@
|
||||||
"status.bookmark": "Bogmærk",
|
"status.bookmark": "Bogmærk",
|
||||||
"status.cancel_reblog_private": "Fjern boost",
|
"status.cancel_reblog_private": "Fjern boost",
|
||||||
"status.cannot_reblog": "Dette indlæg kan ikke fremhæves",
|
"status.cannot_reblog": "Dette indlæg kan ikke fremhæves",
|
||||||
|
"status.continued_thread": "Fortsat tråd",
|
||||||
"status.copy": "Kopiér link til indlæg",
|
"status.copy": "Kopiér link til indlæg",
|
||||||
"status.delete": "Slet",
|
"status.delete": "Slet",
|
||||||
"status.detailed_status": "Detaljeret samtalevisning",
|
"status.detailed_status": "Detaljeret samtalevisning",
|
||||||
|
@ -813,6 +814,7 @@
|
||||||
"status.reblogs.empty": "Ingen har endnu fremhævet dette indlæg. Når nogen gør, vil det fremgå hér.",
|
"status.reblogs.empty": "Ingen har endnu fremhævet dette indlæg. Når nogen gør, vil det fremgå hér.",
|
||||||
"status.redraft": "Slet og omformulér",
|
"status.redraft": "Slet og omformulér",
|
||||||
"status.remove_bookmark": "Fjern bogmærke",
|
"status.remove_bookmark": "Fjern bogmærke",
|
||||||
|
"status.replied_in_thread": "Svaret i tråd",
|
||||||
"status.replied_to": "Besvarede {name}",
|
"status.replied_to": "Besvarede {name}",
|
||||||
"status.reply": "Besvar",
|
"status.reply": "Besvar",
|
||||||
"status.replyAll": "Besvar alle",
|
"status.replyAll": "Besvar alle",
|
||||||
|
|
|
@ -457,7 +457,7 @@
|
||||||
"lists.subheading": "Deine Listen",
|
"lists.subheading": "Deine Listen",
|
||||||
"load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}",
|
"load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}",
|
||||||
"loading_indicator.label": "Wird geladen …",
|
"loading_indicator.label": "Wird geladen …",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Medium ausblenden} other {Medien ausblenden}}",
|
"media_gallery.hide": "Ausblenden",
|
||||||
"moved_to_account_banner.text": "Dein Konto {disabledAccount} ist derzeit deaktiviert, weil du zu {movedToAccount} umgezogen bist.",
|
"moved_to_account_banner.text": "Dein Konto {disabledAccount} ist derzeit deaktiviert, weil du zu {movedToAccount} umgezogen bist.",
|
||||||
"mute_modal.hide_from_notifications": "Benachrichtigungen ausblenden",
|
"mute_modal.hide_from_notifications": "Benachrichtigungen ausblenden",
|
||||||
"mute_modal.hide_options": "Einstellungen ausblenden",
|
"mute_modal.hide_options": "Einstellungen ausblenden",
|
||||||
|
@ -780,6 +780,7 @@
|
||||||
"status.bookmark": "Beitrag als Lesezeichen setzen",
|
"status.bookmark": "Beitrag als Lesezeichen setzen",
|
||||||
"status.cancel_reblog_private": "Beitrag nicht mehr teilen",
|
"status.cancel_reblog_private": "Beitrag nicht mehr teilen",
|
||||||
"status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
|
"status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
|
||||||
|
"status.continued_thread": "Fortgeführter Thread",
|
||||||
"status.copy": "Link zum Beitrag kopieren",
|
"status.copy": "Link zum Beitrag kopieren",
|
||||||
"status.delete": "Beitrag löschen",
|
"status.delete": "Beitrag löschen",
|
||||||
"status.detailed_status": "Detaillierte Ansicht der Unterhaltung",
|
"status.detailed_status": "Detaillierte Ansicht der Unterhaltung",
|
||||||
|
@ -813,6 +814,7 @@
|
||||||
"status.reblogs.empty": "Diesen Beitrag hat bisher noch niemand geteilt. Sobald es jemand tut, wird das Profil hier erscheinen.",
|
"status.reblogs.empty": "Diesen Beitrag hat bisher noch niemand geteilt. Sobald es jemand tut, wird das Profil hier erscheinen.",
|
||||||
"status.redraft": "Löschen und neu erstellen",
|
"status.redraft": "Löschen und neu erstellen",
|
||||||
"status.remove_bookmark": "Lesezeichen entfernen",
|
"status.remove_bookmark": "Lesezeichen entfernen",
|
||||||
|
"status.replied_in_thread": "Antwortete im Thread",
|
||||||
"status.replied_to": "Antwortete {name}",
|
"status.replied_to": "Antwortete {name}",
|
||||||
"status.reply": "Antworten",
|
"status.reply": "Antworten",
|
||||||
"status.replyAll": "Allen antworten",
|
"status.replyAll": "Allen antworten",
|
||||||
|
|
|
@ -452,7 +452,6 @@
|
||||||
"lists.subheading": "Οι λίστες σου",
|
"lists.subheading": "Οι λίστες σου",
|
||||||
"load_pending": "{count, plural, one {# νέο στοιχείο} other {# νέα στοιχεία}}",
|
"load_pending": "{count, plural, one {# νέο στοιχείο} other {# νέα στοιχεία}}",
|
||||||
"loading_indicator.label": "Φόρτωση…",
|
"loading_indicator.label": "Φόρτωση…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Απόκρυψη εικόνας} other {Απόκρυψη εικόνων}}",
|
|
||||||
"moved_to_account_banner.text": "Ο λογαριασμός σου {disabledAccount} είναι προσωρινά απενεργοποιημένος επειδή μεταφέρθηκες στον {movedToAccount}.",
|
"moved_to_account_banner.text": "Ο λογαριασμός σου {disabledAccount} είναι προσωρινά απενεργοποιημένος επειδή μεταφέρθηκες στον {movedToAccount}.",
|
||||||
"mute_modal.hide_from_notifications": "Απόκρυψη από ειδοποιήσεις",
|
"mute_modal.hide_from_notifications": "Απόκρυψη από ειδοποιήσεις",
|
||||||
"mute_modal.hide_options": "Απόκρυψη επιλογών",
|
"mute_modal.hide_options": "Απόκρυψη επιλογών",
|
||||||
|
|
|
@ -457,7 +457,7 @@
|
||||||
"lists.subheading": "Your lists",
|
"lists.subheading": "Your lists",
|
||||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||||
"loading_indicator.label": "Loading…",
|
"loading_indicator.label": "Loading…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
|
"media_gallery.hide": "Hide",
|
||||||
"moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
|
"moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
|
||||||
"mute_modal.hide_from_notifications": "Hide from notifications",
|
"mute_modal.hide_from_notifications": "Hide from notifications",
|
||||||
"mute_modal.hide_options": "Hide options",
|
"mute_modal.hide_options": "Hide options",
|
||||||
|
@ -780,6 +780,7 @@
|
||||||
"status.bookmark": "Bookmark",
|
"status.bookmark": "Bookmark",
|
||||||
"status.cancel_reblog_private": "Unboost",
|
"status.cancel_reblog_private": "Unboost",
|
||||||
"status.cannot_reblog": "This post cannot be boosted",
|
"status.cannot_reblog": "This post cannot be boosted",
|
||||||
|
"status.continued_thread": "Continued thread",
|
||||||
"status.copy": "Copy link to status",
|
"status.copy": "Copy link to status",
|
||||||
"status.delete": "Delete",
|
"status.delete": "Delete",
|
||||||
"status.detailed_status": "Detailed conversation view",
|
"status.detailed_status": "Detailed conversation view",
|
||||||
|
@ -813,6 +814,7 @@
|
||||||
"status.reblogs.empty": "No one has boosted this post yet. When someone does, they will show up here.",
|
"status.reblogs.empty": "No one has boosted this post yet. When someone does, they will show up here.",
|
||||||
"status.redraft": "Delete & re-draft",
|
"status.redraft": "Delete & re-draft",
|
||||||
"status.remove_bookmark": "Remove bookmark",
|
"status.remove_bookmark": "Remove bookmark",
|
||||||
|
"status.replied_in_thread": "Replied in thread",
|
||||||
"status.replied_to": "Replied to {name}",
|
"status.replied_to": "Replied to {name}",
|
||||||
"status.reply": "Reply",
|
"status.reply": "Reply",
|
||||||
"status.replyAll": "Reply to thread",
|
"status.replyAll": "Reply to thread",
|
||||||
|
|
|
@ -457,7 +457,7 @@
|
||||||
"lists.subheading": "Your lists",
|
"lists.subheading": "Your lists",
|
||||||
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
"load_pending": "{count, plural, one {# new item} other {# new items}}",
|
||||||
"loading_indicator.label": "Loading…",
|
"loading_indicator.label": "Loading…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Hide image} other {Hide images}}",
|
"media_gallery.hide": "Hide",
|
||||||
"moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
|
"moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.",
|
||||||
"mute_modal.hide_from_notifications": "Hide from notifications",
|
"mute_modal.hide_from_notifications": "Hide from notifications",
|
||||||
"mute_modal.hide_options": "Hide options",
|
"mute_modal.hide_options": "Hide options",
|
||||||
|
@ -780,6 +780,7 @@
|
||||||
"status.bookmark": "Bookmark",
|
"status.bookmark": "Bookmark",
|
||||||
"status.cancel_reblog_private": "Unboost",
|
"status.cancel_reblog_private": "Unboost",
|
||||||
"status.cannot_reblog": "This post cannot be boosted",
|
"status.cannot_reblog": "This post cannot be boosted",
|
||||||
|
"status.continued_thread": "Continued thread",
|
||||||
"status.copy": "Copy link to post",
|
"status.copy": "Copy link to post",
|
||||||
"status.delete": "Delete",
|
"status.delete": "Delete",
|
||||||
"status.detailed_status": "Detailed conversation view",
|
"status.detailed_status": "Detailed conversation view",
|
||||||
|
@ -788,7 +789,7 @@
|
||||||
"status.edit": "Edit",
|
"status.edit": "Edit",
|
||||||
"status.edited": "Last edited {date}",
|
"status.edited": "Last edited {date}",
|
||||||
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
|
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
|
||||||
"status.embed": "Embed",
|
"status.embed": "Get embed code",
|
||||||
"status.favourite": "Favorite",
|
"status.favourite": "Favorite",
|
||||||
"status.favourites": "{count, plural, one {favorite} other {favorites}}",
|
"status.favourites": "{count, plural, one {favorite} other {favorites}}",
|
||||||
"status.filter": "Filter this post",
|
"status.filter": "Filter this post",
|
||||||
|
@ -813,6 +814,7 @@
|
||||||
"status.reblogs.empty": "No one has boosted this post yet. When someone does, they will show up here.",
|
"status.reblogs.empty": "No one has boosted this post yet. When someone does, they will show up here.",
|
||||||
"status.redraft": "Delete & re-draft",
|
"status.redraft": "Delete & re-draft",
|
||||||
"status.remove_bookmark": "Remove bookmark",
|
"status.remove_bookmark": "Remove bookmark",
|
||||||
|
"status.replied_in_thread": "Replied in thread",
|
||||||
"status.replied_to": "Replied to {name}",
|
"status.replied_to": "Replied to {name}",
|
||||||
"status.reply": "Reply",
|
"status.reply": "Reply",
|
||||||
"status.replyAll": "Reply to thread",
|
"status.replyAll": "Reply to thread",
|
||||||
|
|
|
@ -390,7 +390,6 @@
|
||||||
"lists.subheading": "Viaj listoj",
|
"lists.subheading": "Viaj listoj",
|
||||||
"load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}",
|
"load_pending": "{count,plural, one {# nova elemento} other {# novaj elementoj}}",
|
||||||
"loading_indicator.label": "Ŝargado…",
|
"loading_indicator.label": "Ŝargado…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Kaŝi la bildon} other {Kaŝi la bildojn}}",
|
|
||||||
"moved_to_account_banner.text": "Via konto {disabledAccount} estas malvalidigita ĉar vi movis ĝin al {movedToAccount}.",
|
"moved_to_account_banner.text": "Via konto {disabledAccount} estas malvalidigita ĉar vi movis ĝin al {movedToAccount}.",
|
||||||
"navigation_bar.about": "Pri",
|
"navigation_bar.about": "Pri",
|
||||||
"navigation_bar.advanced_interface": "Malfermi altnivelan retpaĝan interfacon",
|
"navigation_bar.advanced_interface": "Malfermi altnivelan retpaĝan interfacon",
|
||||||
|
|
|
@ -457,7 +457,7 @@
|
||||||
"lists.subheading": "Tus listas",
|
"lists.subheading": "Tus listas",
|
||||||
"load_pending": "{count, plural, one {# elemento nuevo} other {# elementos nuevos}}",
|
"load_pending": "{count, plural, one {# elemento nuevo} other {# elementos nuevos}}",
|
||||||
"loading_indicator.label": "Cargando…",
|
"loading_indicator.label": "Cargando…",
|
||||||
"media_gallery.toggle_visible": "Ocultar {number, plural, one {imagen} other {imágenes}}",
|
"media_gallery.hide": "Ocultar",
|
||||||
"moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te mudaste a {movedToAccount}.",
|
"moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te mudaste a {movedToAccount}.",
|
||||||
"mute_modal.hide_from_notifications": "Ocultar en las notificaciones",
|
"mute_modal.hide_from_notifications": "Ocultar en las notificaciones",
|
||||||
"mute_modal.hide_options": "Ocultar opciones",
|
"mute_modal.hide_options": "Ocultar opciones",
|
||||||
|
@ -780,6 +780,7 @@
|
||||||
"status.bookmark": "Marcar",
|
"status.bookmark": "Marcar",
|
||||||
"status.cancel_reblog_private": "Quitar adhesión",
|
"status.cancel_reblog_private": "Quitar adhesión",
|
||||||
"status.cannot_reblog": "No se puede adherir a este mensaje",
|
"status.cannot_reblog": "No se puede adherir a este mensaje",
|
||||||
|
"status.continued_thread": "Continuación de hilo",
|
||||||
"status.copy": "Copiar enlace al mensaje",
|
"status.copy": "Copiar enlace al mensaje",
|
||||||
"status.delete": "Eliminar",
|
"status.delete": "Eliminar",
|
||||||
"status.detailed_status": "Vista de conversación detallada",
|
"status.detailed_status": "Vista de conversación detallada",
|
||||||
|
@ -813,6 +814,7 @@
|
||||||
"status.reblogs.empty": "Todavía nadie adhirió a este mensaje. Cuando alguien lo haga, se mostrará acá.",
|
"status.reblogs.empty": "Todavía nadie adhirió a este mensaje. Cuando alguien lo haga, se mostrará acá.",
|
||||||
"status.redraft": "Eliminar mensaje original y editarlo",
|
"status.redraft": "Eliminar mensaje original y editarlo",
|
||||||
"status.remove_bookmark": "Quitar marcador",
|
"status.remove_bookmark": "Quitar marcador",
|
||||||
|
"status.replied_in_thread": "Respuesta en hilo",
|
||||||
"status.replied_to": "Respondió a {name}",
|
"status.replied_to": "Respondió a {name}",
|
||||||
"status.reply": "Responder",
|
"status.reply": "Responder",
|
||||||
"status.replyAll": "Responder al hilo",
|
"status.replyAll": "Responder al hilo",
|
||||||
|
|
|
@ -457,7 +457,6 @@
|
||||||
"lists.subheading": "Tus listas",
|
"lists.subheading": "Tus listas",
|
||||||
"load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
|
"load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
|
||||||
"loading_indicator.label": "Cargando…",
|
"loading_indicator.label": "Cargando…",
|
||||||
"media_gallery.toggle_visible": "Cambiar visibilidad",
|
|
||||||
"moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.",
|
"moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.",
|
||||||
"mute_modal.hide_from_notifications": "Ocultar de las notificaciones",
|
"mute_modal.hide_from_notifications": "Ocultar de las notificaciones",
|
||||||
"mute_modal.hide_options": "Ocultar opciones",
|
"mute_modal.hide_options": "Ocultar opciones",
|
||||||
|
|
|
@ -457,7 +457,6 @@
|
||||||
"lists.subheading": "Tus listas",
|
"lists.subheading": "Tus listas",
|
||||||
"load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
|
"load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}",
|
||||||
"loading_indicator.label": "Cargando…",
|
"loading_indicator.label": "Cargando…",
|
||||||
"media_gallery.toggle_visible": "Cambiar visibilidad",
|
|
||||||
"moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.",
|
"moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.",
|
||||||
"mute_modal.hide_from_notifications": "Ocultar de las notificaciones",
|
"mute_modal.hide_from_notifications": "Ocultar de las notificaciones",
|
||||||
"mute_modal.hide_options": "Ocultar opciones",
|
"mute_modal.hide_options": "Ocultar opciones",
|
||||||
|
|
|
@ -353,6 +353,14 @@
|
||||||
"hashtag.follow": "Jälgi silti",
|
"hashtag.follow": "Jälgi silti",
|
||||||
"hashtag.unfollow": "Lõpeta sildi jälgimine",
|
"hashtag.unfollow": "Lõpeta sildi jälgimine",
|
||||||
"hashtags.and_other": "…ja {count, plural, one {}other {# veel}}",
|
"hashtags.and_other": "…ja {count, plural, one {}other {# veel}}",
|
||||||
|
"hints.profiles.followers_may_be_missing": "Selle profiili jälgijaid võib olla puudu.",
|
||||||
|
"hints.profiles.follows_may_be_missing": "Selle profiili poolt jälgitavaid võib olla puudu.",
|
||||||
|
"hints.profiles.posts_may_be_missing": "Mõned selle profiili postitused võivad olla puudu.",
|
||||||
|
"hints.profiles.see_more_followers": "Vaata rohkem jälgijaid kohas {domain}",
|
||||||
|
"hints.profiles.see_more_follows": "Vaata rohkem jälgitavaid kohas {domain}",
|
||||||
|
"hints.profiles.see_more_posts": "Vaata rohkem postitusi kohas {domain}",
|
||||||
|
"hints.threads.replies_may_be_missing": "Vastuseid teistest serveritest võib olla puudu.",
|
||||||
|
"hints.threads.see_more": "Vaata rohkem vastuseid kohas {domain}",
|
||||||
"home.column_settings.show_reblogs": "Näita jagamisi",
|
"home.column_settings.show_reblogs": "Näita jagamisi",
|
||||||
"home.column_settings.show_replies": "Näita vastuseid",
|
"home.column_settings.show_replies": "Näita vastuseid",
|
||||||
"home.hide_announcements": "Peida teadaanded",
|
"home.hide_announcements": "Peida teadaanded",
|
||||||
|
@ -360,6 +368,17 @@
|
||||||
"home.pending_critical_update.link": "Vaata uuendusi",
|
"home.pending_critical_update.link": "Vaata uuendusi",
|
||||||
"home.pending_critical_update.title": "Saadaval kriitiline turvauuendus!",
|
"home.pending_critical_update.title": "Saadaval kriitiline turvauuendus!",
|
||||||
"home.show_announcements": "Kuva teadaandeid",
|
"home.show_announcements": "Kuva teadaandeid",
|
||||||
|
"ignore_notifications_modal.disclaimer": "Mastodon ei saa teavitada kasutajaid, et ignoreerisid nende teavitusi. Teavituste ignoreerimine ei peata sõnumite endi saatmist.",
|
||||||
|
"ignore_notifications_modal.filter_instead": "Selle asemel filtreeri",
|
||||||
|
"ignore_notifications_modal.filter_to_act_users": "Saad endiselt kasutajaid vastu võtta, tagasi lükata või neist teatada",
|
||||||
|
"ignore_notifications_modal.filter_to_avoid_confusion": "Filtreerimine aitab vältida võimalikke segaminiajamisi",
|
||||||
|
"ignore_notifications_modal.filter_to_review_separately": "Saad filtreeritud teateid eraldi vaadata",
|
||||||
|
"ignore_notifications_modal.ignore": "Ignoreeri teavitusi",
|
||||||
|
"ignore_notifications_modal.limited_accounts_title": "Ignoreeri modereeritud kontode teavitusi?",
|
||||||
|
"ignore_notifications_modal.new_accounts_title": "Ignoreeri uute kontode teavitusi?",
|
||||||
|
"ignore_notifications_modal.not_followers_title": "Ignoreeri inimeste teavitusi, kes sind ei jälgi?",
|
||||||
|
"ignore_notifications_modal.not_following_title": "Ignoreeri inimeste teavitusi, keda sa ei jälgi?",
|
||||||
|
"ignore_notifications_modal.private_mentions_title": "Ignoreeri soovimatute eraviisiliste mainimiste teateid?",
|
||||||
"interaction_modal.description.favourite": "Mastodoni kontoga saad postituse lemmikuks märkida, et autor teaks, et sa hindad seda, ja jätta see hiljemaks alles.",
|
"interaction_modal.description.favourite": "Mastodoni kontoga saad postituse lemmikuks märkida, et autor teaks, et sa hindad seda, ja jätta see hiljemaks alles.",
|
||||||
"interaction_modal.description.follow": "Mastodoni kontoga saad jälgida kasutajat {name}, et tema postitusi oma koduvoos näha.",
|
"interaction_modal.description.follow": "Mastodoni kontoga saad jälgida kasutajat {name}, et tema postitusi oma koduvoos näha.",
|
||||||
"interaction_modal.description.reblog": "Mastodoni kontoga saad seda postitust levitada, jagades seda oma jälgijatele.",
|
"interaction_modal.description.reblog": "Mastodoni kontoga saad seda postitust levitada, jagades seda oma jälgijatele.",
|
||||||
|
@ -438,7 +457,6 @@
|
||||||
"lists.subheading": "Sinu nimekirjad",
|
"lists.subheading": "Sinu nimekirjad",
|
||||||
"load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}",
|
"load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}",
|
||||||
"loading_indicator.label": "Laadimine…",
|
"loading_indicator.label": "Laadimine…",
|
||||||
"media_gallery.toggle_visible": "{number, plural, one {Varja pilt} other {Varja pildid}}",
|
|
||||||
"moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.",
|
"moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.",
|
||||||
"mute_modal.hide_from_notifications": "Peida teavituste hulgast",
|
"mute_modal.hide_from_notifications": "Peida teavituste hulgast",
|
||||||
"mute_modal.hide_options": "Peida valikud",
|
"mute_modal.hide_options": "Peida valikud",
|
||||||
|
@ -483,9 +501,13 @@
|
||||||
"notification.admin.report_statuses": "{name} raporteeris {target} kategooriast {category}",
|
"notification.admin.report_statuses": "{name} raporteeris {target} kategooriast {category}",
|
||||||
"notification.admin.report_statuses_other": "{name} raporteeris kohast {target}",
|
"notification.admin.report_statuses_other": "{name} raporteeris kohast {target}",
|
||||||
"notification.admin.sign_up": "{name} registreerus",
|
"notification.admin.sign_up": "{name} registreerus",
|
||||||
|
"notification.admin.sign_up.name_and_others": "{name} ja {count, plural, one {# veel} other {# teist}} liitus",
|
||||||
"notification.favourite": "{name} märkis su postituse lemmikuks",
|
"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.follow": "{name} alustas su jälgimist",
|
"notification.follow": "{name} alustas su jälgimist",
|
||||||
|
"notification.follow.name_and_others": "{name} ja {count, plural, one {# veel} other {# teist}} hakkas sind jälgima",
|
||||||
"notification.follow_request": "{name} soovib sind jälgida",
|
"notification.follow_request": "{name} soovib sind jälgida",
|
||||||
|
"notification.follow_request.name_and_others": "{name} ja {count, plural, one {# veel} other {# teist}} taotles sinu jälgimist",
|
||||||
"notification.label.mention": "Mainimine",
|
"notification.label.mention": "Mainimine",
|
||||||
"notification.label.private_mention": "Privaatne mainimine",
|
"notification.label.private_mention": "Privaatne mainimine",
|
||||||
"notification.label.private_reply": "Privaatne vastus",
|
"notification.label.private_reply": "Privaatne vastus",
|
||||||
|
@ -503,6 +525,7 @@
|
||||||
"notification.own_poll": "Su küsitlus on lõppenud",
|
"notification.own_poll": "Su küsitlus on lõppenud",
|
||||||
"notification.poll": "Hääletus, millel osalesid, on lõppenud",
|
"notification.poll": "Hääletus, millel osalesid, on lõppenud",
|
||||||
"notification.reblog": "{name} jagas edasi postitust",
|
"notification.reblog": "{name} jagas edasi postitust",
|
||||||
|
"notification.reblog.name_and_others_with_link": "{name} ja <a>{count, plural, one {# veel} other {# teist}}</a> jagas su postitust",
|
||||||
"notification.relationships_severance_event": "Kadunud ühendus kasutajaga {name}",
|
"notification.relationships_severance_event": "Kadunud ühendus kasutajaga {name}",
|
||||||
"notification.relationships_severance_event.account_suspension": "{from} admin on kustutanud {target}, mis tähendab, et sa ei saa enam neilt uuendusi või suhelda nendega.",
|
"notification.relationships_severance_event.account_suspension": "{from} admin on kustutanud {target}, mis tähendab, et sa ei saa enam neilt uuendusi või suhelda nendega.",
|
||||||
"notification.relationships_severance_event.domain_block": "{from} admin on blokeerinud {target}, sealhulgas {followersCount} sinu jälgijat ja {followingCount, plural, one {# konto} other {# kontot}}, mida jälgid.",
|
"notification.relationships_severance_event.domain_block": "{from} admin on blokeerinud {target}, sealhulgas {followersCount} sinu jälgijat ja {followingCount, plural, one {# konto} other {# kontot}}, mida jälgid.",
|
||||||
|
@ -511,13 +534,24 @@
|
||||||
"notification.status": "{name} just postitas",
|
"notification.status": "{name} just postitas",
|
||||||
"notification.update": "{name} muutis postitust",
|
"notification.update": "{name} muutis postitust",
|
||||||
"notification_requests.accept": "Nõus",
|
"notification_requests.accept": "Nõus",
|
||||||
|
"notification_requests.accept_multiple": "{count, plural, one {Nõustu # taotlusega…} other {Nõustu # taotlusega…}}",
|
||||||
|
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Nõustu taotlusega} other {Nõustu taotlustega}}",
|
||||||
|
"notification_requests.confirm_accept_multiple.message": "Oled nõustumas {count, plural, one {ühe teavituse taotlusega} other {# teavituse taotlusega}}. Oled kindel, et soovid jätkata?",
|
||||||
|
"notification_requests.confirm_accept_multiple.title": "Nõustuda teavituste taotlustega?",
|
||||||
|
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Loobu taotlusest} other {Loobu taotlustest}}",
|
||||||
|
"notification_requests.confirm_dismiss_multiple.message": "Oled loobumas {count, plural, one {ühest teavituse taotlusest} other {# teavituse taotlusest}}. {count, plural, one {Sellele} other {Neile}} pole hiljem lihtne ligi pääseda. Oled kindel, et soovid jätkata?",
|
||||||
|
"notification_requests.confirm_dismiss_multiple.title": "Hüljata teavituse taotlused?",
|
||||||
"notification_requests.dismiss": "Hülga",
|
"notification_requests.dismiss": "Hülga",
|
||||||
|
"notification_requests.dismiss_multiple": "{count, plural, one {Loobuda # taotlusest…} other {Loobuda # taotlusest…}}",
|
||||||
|
"notification_requests.edit_selection": "Muuda",
|
||||||
|
"notification_requests.exit_selection": "Valmis",
|
||||||
"notification_requests.explainer_for_limited_account": "Sellelt kontolt tulevad teavitused on filtreeritud, sest moderaator on seda kontot piiranud.",
|
"notification_requests.explainer_for_limited_account": "Sellelt kontolt tulevad teavitused on filtreeritud, sest moderaator on seda kontot piiranud.",
|
||||||
"notification_requests.explainer_for_limited_remote_account": "Sellelt kontolt tulevad teavitused on filtreeritud, sest moderaator on seda kontot või serverit piiranud.",
|
"notification_requests.explainer_for_limited_remote_account": "Sellelt kontolt tulevad teavitused on filtreeritud, sest moderaator on seda kontot või serverit piiranud.",
|
||||||
"notification_requests.maximize": "Maksimeeri",
|
"notification_requests.maximize": "Maksimeeri",
|
||||||
"notification_requests.minimize_banner": "Minimeeri filtreeritud teavituste bänner",
|
"notification_requests.minimize_banner": "Minimeeri filtreeritud teavituste bänner",
|
||||||
"notification_requests.notifications_from": "Teavitus kasutajalt {name}",
|
"notification_requests.notifications_from": "Teavitus kasutajalt {name}",
|
||||||
"notification_requests.title": "Filtreeritud teavitused",
|
"notification_requests.title": "Filtreeritud teavitused",
|
||||||
|
"notification_requests.view": "Vaata teavitusi",
|
||||||
"notifications.clear": "Puhasta teated",
|
"notifications.clear": "Puhasta teated",
|
||||||
"notifications.clear_confirmation": "Oled kindel, et soovid püsivalt kõik oma teated eemaldada?",
|
"notifications.clear_confirmation": "Oled kindel, et soovid püsivalt kõik oma teated eemaldada?",
|
||||||
"notifications.clear_title": "Tühjenda teavitus?",
|
"notifications.clear_title": "Tühjenda teavitus?",
|
||||||
|
@ -554,6 +588,12 @@
|
||||||
"notifications.permission_denied": "Töölauamärguanded pole saadaval, kuna eelnevalt keelduti lehitsejale teavituste luba andmast",
|
"notifications.permission_denied": "Töölauamärguanded pole saadaval, kuna eelnevalt keelduti lehitsejale teavituste luba andmast",
|
||||||
"notifications.permission_denied_alert": "Töölaua märguandeid ei saa lubada, kuna brauseri luba on varem keeldutud",
|
"notifications.permission_denied_alert": "Töölaua märguandeid ei saa lubada, kuna brauseri luba on varem keeldutud",
|
||||||
"notifications.permission_required": "Töölaua märguanded ei ole saadaval, kuna vajalik luba pole antud.",
|
"notifications.permission_required": "Töölaua märguanded ei ole saadaval, kuna vajalik luba pole antud.",
|
||||||
|
"notifications.policy.accept": "Nõustun",
|
||||||
|
"notifications.policy.accept_hint": "Näita teavitustes",
|
||||||
|
"notifications.policy.drop": "Ignoreeri",
|
||||||
|
"notifications.policy.drop_hint": "Saada tühjusse, mitte kunagi seda enam näha",
|
||||||
|
"notifications.policy.filter": "Filter",
|
||||||
|
"notifications.policy.filter_hint": "Saadetud filtreeritud teavituste sisendkasti",
|
||||||
"notifications.policy.filter_limited_accounts_hint": "Piiratud serveri moderaatorite poolt",
|
"notifications.policy.filter_limited_accounts_hint": "Piiratud serveri moderaatorite poolt",
|
||||||
"notifications.policy.filter_limited_accounts_title": "Modereeritud kontod",
|
"notifications.policy.filter_limited_accounts_title": "Modereeritud kontod",
|
||||||
"notifications.policy.filter_new_accounts.hint": "Loodud viimase {days, plural, one {ühe päeva} other {# päeva}} jooksul",
|
"notifications.policy.filter_new_accounts.hint": "Loodud viimase {days, plural, one {ühe päeva} other {# päeva}} jooksul",
|
||||||
|
@ -564,6 +604,7 @@
|
||||||
"notifications.policy.filter_not_following_title": "Inimesed, keda sa ei jälgi",
|
"notifications.policy.filter_not_following_title": "Inimesed, keda sa ei jälgi",
|
||||||
"notifications.policy.filter_private_mentions_hint": "Filtreeritud, kui see pole vastus sinupoolt mainimisele või kui jälgid saatjat",
|
"notifications.policy.filter_private_mentions_hint": "Filtreeritud, kui see pole vastus sinupoolt mainimisele või kui jälgid saatjat",
|
||||||
"notifications.policy.filter_private_mentions_title": "Soovimatud privaatsed mainimised",
|
"notifications.policy.filter_private_mentions_title": "Soovimatud privaatsed mainimised",
|
||||||
|
"notifications.policy.title": "Halda teavitusi kohast…",
|
||||||
"notifications_permission_banner.enable": "Luba töölaua märguanded",
|
"notifications_permission_banner.enable": "Luba töölaua märguanded",
|
||||||
"notifications_permission_banner.how_to_control": "Et saada teateid, ajal mil Mastodon pole avatud, luba töölauamärguanded. Saad täpselt määrata, mis tüüpi tegevused tekitavad märguandeid, kasutates peale teadaannete sisse lülitamist üleval olevat nuppu {icon}.",
|
"notifications_permission_banner.how_to_control": "Et saada teateid, ajal mil Mastodon pole avatud, luba töölauamärguanded. Saad täpselt määrata, mis tüüpi tegevused tekitavad märguandeid, kasutates peale teadaannete sisse lülitamist üleval olevat nuppu {icon}.",
|
||||||
"notifications_permission_banner.title": "Ära jää millestki ilma",
|
"notifications_permission_banner.title": "Ära jää millestki ilma",
|
||||||
|
|
|
@ -457,7 +457,6 @@
|
||||||
"lists.subheading": "Zure zerrendak",
|
"lists.subheading": "Zure zerrendak",
|
||||||
"load_pending": "{count, plural, one {elementu berri #} other {# elementu berri}}",
|
"load_pending": "{count, plural, one {elementu berri #} other {# elementu berri}}",
|
||||||
"loading_indicator.label": "Kargatzen…",
|
"loading_indicator.label": "Kargatzen…",
|
||||||
"media_gallery.toggle_visible": "Txandakatu ikusgaitasuna",
|
|
||||||
"moved_to_account_banner.text": "Zure {disabledAccount} kontua desgaituta dago une honetan, {movedToAccount} kontura aldatu zinelako.",
|
"moved_to_account_banner.text": "Zure {disabledAccount} kontua desgaituta dago une honetan, {movedToAccount} kontura aldatu zinelako.",
|
||||||
"mute_modal.hide_from_notifications": "Ezkutatu jakinarazpenetatik",
|
"mute_modal.hide_from_notifications": "Ezkutatu jakinarazpenetatik",
|
||||||
"mute_modal.hide_options": "Ezkutatu aukerak",
|
"mute_modal.hide_options": "Ezkutatu aukerak",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue