Merge commit '26e7fe97714d077930621f9111b7eaad2774df65' into glitch-soc/merge-upstream
This commit is contained in:
commit
a7ba4ba446
2
.github/actions/setup-javascript/action.yml
vendored
2
.github/actions/setup-javascript/action.yml
vendored
@ -9,7 +9,7 @@ runs:
|
|||||||
using: 'composite'
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
|
|||||||
8
.github/workflows/build-container-image.yml
vendored
8
.github/workflows/build-container-image.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
|||||||
- linux/arm64
|
- linux/arm64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Prepare
|
- name: Prepare
|
||||||
env:
|
env:
|
||||||
@ -100,7 +100,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload digest
|
- name: Upload digest
|
||||||
if: ${{ inputs.push_to_images != '' }}
|
if: ${{ inputs.push_to_images != '' }}
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
||||||
name: digests-${{ hashFiles(inputs.file_to_build) }}-${{ env.PLATFORM_PAIR }}
|
name: digests-${{ hashFiles(inputs.file_to_build) }}-${{ env.PLATFORM_PAIR }}
|
||||||
@ -119,10 +119,10 @@ jobs:
|
|||||||
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
|
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: ${{ runner.temp }}/digests
|
path: ${{ runner.temp }}/digests
|
||||||
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
||||||
|
|||||||
2
.github/workflows/build-push-pr.yml
vendored
2
.github/workflows/build-push-pr.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
# Repository needs to be cloned so `git rev-parse` below works
|
# Repository needs to be cloned so `git rev-parse` below works
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
- id: version_vars
|
- id: version_vars
|
||||||
run: |
|
run: |
|
||||||
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short ${{github.event.pull_request.head.sha}}) >> $GITHUB_OUTPUT
|
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short ${{github.event.pull_request.head.sha}}) >> $GITHUB_OUTPUT
|
||||||
|
|||||||
2
.github/workflows/bundler-audit.yml
vendored
2
.github/workflows/bundler-audit.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
|
|||||||
2
.github/workflows/check-i18n.yml
vendored
2
.github/workflows/check-i18n.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Ruby environment
|
- name: Set up Ruby environment
|
||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|||||||
4
.github/workflows/chromatic.yml
vendored
4
.github/workflows/chromatic.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
if: github.repository == 'mastodon/mastodon'
|
if: github.repository == 'mastodon/mastodon'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
run: yarn build-storybook
|
run: yarn build-storybook
|
||||||
|
|
||||||
- name: Run Chromatic
|
- name: Run Chromatic
|
||||||
uses: chromaui/action@v12
|
uses: chromaui/action@v13
|
||||||
with:
|
with:
|
||||||
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
|
# ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
|
||||||
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||||
|
|||||||
8
.github/workflows/codeql.yml
vendored
8
.github/workflows/codeql.yml
vendored
@ -31,11 +31,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@ -48,7 +48,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v4
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@ -61,6 +61,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Increase Git http.postBuffer
|
- name: Increase Git http.postBuffer
|
||||||
# This is needed due to a bug in Ubuntu's cURL version?
|
# This is needed due to a bug in Ubuntu's cURL version?
|
||||||
|
|||||||
2
.github/workflows/crowdin-download.yml
vendored
2
.github/workflows/crowdin-download.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Increase Git http.postBuffer
|
- name: Increase Git http.postBuffer
|
||||||
# This is needed due to a bug in Ubuntu's cURL version?
|
# This is needed due to a bug in Ubuntu's cURL version?
|
||||||
|
|||||||
2
.github/workflows/crowdin-upload.yml
vendored
2
.github/workflows/crowdin-upload.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: crowdin action
|
- name: crowdin action
|
||||||
uses: crowdin/github-action@v2
|
uses: crowdin/github-action@v2
|
||||||
|
|||||||
2
.github/workflows/format-check.yml
vendored
2
.github/workflows/format-check.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|||||||
2
.github/workflows/lint-css.yml
vendored
2
.github/workflows/lint-css.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|||||||
2
.github/workflows/lint-haml.yml
vendored
2
.github/workflows/lint-haml.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
|
|||||||
2
.github/workflows/lint-js.yml
vendored
2
.github/workflows/lint-js.yml
vendored
@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|||||||
2
.github/workflows/lint-ruby.yml
vendored
2
.github/workflows/lint-ruby.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Ruby
|
- name: Set up Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
|
|||||||
2
.github/workflows/test-js.yml
vendored
2
.github/workflows/test-js.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Javascript environment
|
- name: Set up Javascript environment
|
||||||
uses: ./.github/actions/setup-javascript
|
uses: ./.github/actions/setup-javascript
|
||||||
|
|||||||
2
.github/workflows/test-migrations.yml
vendored
2
.github/workflows/test-migrations.yml
vendored
@ -72,7 +72,7 @@ jobs:
|
|||||||
BUNDLE_RETRY: 3
|
BUNDLE_RETRY: 3
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Ruby environment
|
- name: Set up Ruby environment
|
||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|||||||
28
.github/workflows/test-ruby.yml
vendored
28
.github/workflows/test-ruby.yml
vendored
@ -32,7 +32,7 @@ jobs:
|
|||||||
SECRET_KEY_BASE_DUMMY: 1
|
SECRET_KEY_BASE_DUMMY: 1
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Set up Ruby environment
|
- name: Set up Ruby environment
|
||||||
uses: ./.github/actions/setup-ruby
|
uses: ./.github/actions/setup-ruby
|
||||||
@ -65,7 +65,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tmp/cache/vite/last-build*.json
|
tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tmp/cache/vite/last-build*.json
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v4
|
- uses: actions/upload-artifact@v5
|
||||||
if: matrix.mode == 'test'
|
if: matrix.mode == 'test'
|
||||||
with:
|
with:
|
||||||
path: |-
|
path: |-
|
||||||
@ -128,9 +128,9 @@ jobs:
|
|||||||
- '3.3'
|
- '3.3'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: './'
|
path: './'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
@ -230,9 +230,9 @@ jobs:
|
|||||||
- '3.3'
|
- '3.3'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: './'
|
path: './'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
@ -309,9 +309,9 @@ jobs:
|
|||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: './'
|
path: './'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
@ -350,14 +350,14 @@ jobs:
|
|||||||
- run: bin/rspec spec/system --tag streaming --tag js
|
- run: bin/rspec spec/system --tag streaming --tag js
|
||||||
|
|
||||||
- name: Archive logs
|
- name: Archive logs
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: e2e-logs-${{ matrix.ruby-version }}
|
name: e2e-logs-${{ matrix.ruby-version }}
|
||||||
path: log/
|
path: log/
|
||||||
|
|
||||||
- name: Archive test screenshots
|
- name: Archive test screenshots
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: e2e-screenshots-${{ matrix.ruby-version }}
|
name: e2e-screenshots-${{ matrix.ruby-version }}
|
||||||
@ -447,9 +447,9 @@ jobs:
|
|||||||
search-image: opensearchproject/opensearch:2
|
search-image: opensearchproject/opensearch:2
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v6
|
||||||
with:
|
with:
|
||||||
path: './'
|
path: './'
|
||||||
name: ${{ github.sha }}
|
name: ${{ github.sha }}
|
||||||
@ -469,14 +469,14 @@ jobs:
|
|||||||
- run: bin/rspec --tag search
|
- run: bin/rspec --tag search
|
||||||
|
|
||||||
- name: Archive logs
|
- name: Archive logs
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: test-search-logs-${{ matrix.ruby-version }}
|
name: test-search-logs-${{ matrix.ruby-version }}
|
||||||
path: log/
|
path: log/
|
||||||
|
|
||||||
- name: Archive test screenshots
|
- name: Archive test screenshots
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
if: failure()
|
if: failure()
|
||||||
with:
|
with:
|
||||||
name: test-search-screenshots
|
name: test-search-screenshots
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const config: StorybookConfig = {
|
|||||||
viteFinal(config) {
|
viteFinal(config) {
|
||||||
// For an unknown reason, Storybook does not use the root
|
// For an unknown reason, Storybook does not use the root
|
||||||
// from the Vite config so we need to set it manually.
|
// from the Vite config so we need to set it manually.
|
||||||
config.root = resolve(__dirname, '../app/javascript');
|
config.root = resolve(import.meta.dirname, '../app/javascript');
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
2
Gemfile
2
Gemfile
@ -114,7 +114,7 @@ group :opentelemetry do
|
|||||||
gem 'opentelemetry-instrumentation-http', '~> 0.27.0', require: false
|
gem 'opentelemetry-instrumentation-http', '~> 0.27.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-http_client', '~> 0.26.0', require: false
|
gem 'opentelemetry-instrumentation-http_client', '~> 0.26.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-net_http', '~> 0.26.0', require: false
|
gem 'opentelemetry-instrumentation-net_http', '~> 0.26.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-pg', '~> 0.32.0', require: false
|
gem 'opentelemetry-instrumentation-pg', '~> 0.33.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-rack', '~> 0.29.0', require: false
|
gem 'opentelemetry-instrumentation-rack', '~> 0.29.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-rails', '~> 0.39.0', require: false
|
gem 'opentelemetry-instrumentation-rails', '~> 0.39.0', require: false
|
||||||
gem 'opentelemetry-instrumentation-redis', '~> 0.28.0', require: false
|
gem 'opentelemetry-instrumentation-redis', '~> 0.28.0', require: false
|
||||||
|
|||||||
20
Gemfile.lock
20
Gemfile.lock
@ -128,7 +128,7 @@ GEM
|
|||||||
blurhash (0.1.8)
|
blurhash (0.1.8)
|
||||||
bootsnap (1.18.6)
|
bootsnap (1.18.6)
|
||||||
msgpack (~> 1.2)
|
msgpack (~> 1.2)
|
||||||
brakeman (7.0.2)
|
brakeman (7.1.1)
|
||||||
racc
|
racc
|
||||||
browser (6.2.0)
|
browser (6.2.0)
|
||||||
builder (3.3.0)
|
builder (3.3.0)
|
||||||
@ -512,9 +512,9 @@ GEM
|
|||||||
opentelemetry-common (~> 0.20)
|
opentelemetry-common (~> 0.20)
|
||||||
opentelemetry-sdk (~> 1.10)
|
opentelemetry-sdk (~> 1.10)
|
||||||
opentelemetry-semantic_conventions
|
opentelemetry-semantic_conventions
|
||||||
opentelemetry-helpers-sql (0.2.0)
|
opentelemetry-helpers-sql (0.3.0)
|
||||||
opentelemetry-api (~> 1.7)
|
opentelemetry-api (~> 1.7)
|
||||||
opentelemetry-helpers-sql-obfuscation (0.4.0)
|
opentelemetry-helpers-sql-obfuscation (0.5.0)
|
||||||
opentelemetry-common (~> 0.21)
|
opentelemetry-common (~> 0.21)
|
||||||
opentelemetry-instrumentation-action_mailer (0.6.1)
|
opentelemetry-instrumentation-action_mailer (0.6.1)
|
||||||
opentelemetry-instrumentation-active_support (~> 0.10)
|
opentelemetry-instrumentation-active_support (~> 0.10)
|
||||||
@ -548,7 +548,7 @@ GEM
|
|||||||
opentelemetry-instrumentation-base (~> 0.25)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-net_http (0.26.0)
|
opentelemetry-instrumentation-net_http (0.26.0)
|
||||||
opentelemetry-instrumentation-base (~> 0.25)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
opentelemetry-instrumentation-pg (0.32.0)
|
opentelemetry-instrumentation-pg (0.33.0)
|
||||||
opentelemetry-helpers-sql
|
opentelemetry-helpers-sql
|
||||||
opentelemetry-helpers-sql-obfuscation
|
opentelemetry-helpers-sql-obfuscation
|
||||||
opentelemetry-instrumentation-base (~> 0.25)
|
opentelemetry-instrumentation-base (~> 0.25)
|
||||||
@ -621,7 +621,7 @@ GEM
|
|||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.2.3)
|
rack (3.2.4)
|
||||||
rack-attack (6.8.0)
|
rack-attack (6.8.0)
|
||||||
rack (>= 1.0, < 4)
|
rack (>= 1.0, < 4)
|
||||||
rack-cors (3.0.0)
|
rack-cors (3.0.0)
|
||||||
@ -803,9 +803,9 @@ GEM
|
|||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
securerandom (0.4.1)
|
securerandom (0.4.1)
|
||||||
shoulda-matchers (6.5.0)
|
shoulda-matchers (7.0.1)
|
||||||
activesupport (>= 5.2.0)
|
activesupport (>= 7.1)
|
||||||
sidekiq (8.0.8)
|
sidekiq (8.0.9)
|
||||||
connection_pool (>= 2.5.0)
|
connection_pool (>= 2.5.0)
|
||||||
json (>= 2.9.0)
|
json (>= 2.9.0)
|
||||||
logger (>= 1.6.2)
|
logger (>= 1.6.2)
|
||||||
@ -883,7 +883,7 @@ GEM
|
|||||||
unicode-display_width (3.2.0)
|
unicode-display_width (3.2.0)
|
||||||
unicode-emoji (~> 4.1)
|
unicode-emoji (~> 4.1)
|
||||||
unicode-emoji (4.1.0)
|
unicode-emoji (4.1.0)
|
||||||
uri (1.0.4)
|
uri (1.1.1)
|
||||||
useragent (0.16.11)
|
useragent (0.16.11)
|
||||||
validate_url (1.0.15)
|
validate_url (1.0.15)
|
||||||
activemodel (>= 3.0.0)
|
activemodel (>= 3.0.0)
|
||||||
@ -1018,7 +1018,7 @@ DEPENDENCIES
|
|||||||
opentelemetry-instrumentation-http (~> 0.27.0)
|
opentelemetry-instrumentation-http (~> 0.27.0)
|
||||||
opentelemetry-instrumentation-http_client (~> 0.26.0)
|
opentelemetry-instrumentation-http_client (~> 0.26.0)
|
||||||
opentelemetry-instrumentation-net_http (~> 0.26.0)
|
opentelemetry-instrumentation-net_http (~> 0.26.0)
|
||||||
opentelemetry-instrumentation-pg (~> 0.32.0)
|
opentelemetry-instrumentation-pg (~> 0.33.0)
|
||||||
opentelemetry-instrumentation-rack (~> 0.29.0)
|
opentelemetry-instrumentation-rack (~> 0.29.0)
|
||||||
opentelemetry-instrumentation-rails (~> 0.39.0)
|
opentelemetry-instrumentation-rails (~> 0.39.0)
|
||||||
opentelemetry-instrumentation-redis (~> 0.28.0)
|
opentelemetry-instrumentation-redis (~> 0.28.0)
|
||||||
|
|||||||
@ -56,7 +56,6 @@ export const COMPOSE_UNMOUNT = 'COMPOSE_UNMOUNT';
|
|||||||
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE';
|
||||||
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE';
|
||||||
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE';
|
||||||
export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE';
|
|
||||||
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
|
export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE';
|
||||||
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
|
export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE';
|
||||||
|
|
||||||
@ -796,13 +795,6 @@ export function changeComposeSpoilerText(text) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function changeComposeVisibility(value) {
|
|
||||||
return {
|
|
||||||
type: COMPOSE_VISIBILITY_CHANGE,
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function insertEmojiCompose(position, emoji, needsSpace) {
|
export function insertEmojiCompose(position, emoji, needsSpace) {
|
||||||
return {
|
return {
|
||||||
type: COMPOSE_EMOJI_INSERT,
|
type: COMPOSE_EMOJI_INSERT,
|
||||||
|
|||||||
@ -13,10 +13,11 @@ import {
|
|||||||
} from 'mastodon/store/typed_functions';
|
} from 'mastodon/store/typed_functions';
|
||||||
|
|
||||||
import type { ApiQuotePolicy } from '../api_types/quotes';
|
import type { ApiQuotePolicy } from '../api_types/quotes';
|
||||||
import type { Status } from '../models/status';
|
import type { Status, StatusVisibility } from '../models/status';
|
||||||
|
import type { RootState } from '../store';
|
||||||
|
|
||||||
import { showAlert } from './alerts';
|
import { showAlert } from './alerts';
|
||||||
import { focusCompose } from './compose';
|
import { changeCompose, focusCompose } from './compose';
|
||||||
import { importFetchedStatuses } from './importer';
|
import { importFetchedStatuses } from './importer';
|
||||||
import { openModal } from './modal';
|
import { openModal } from './modal';
|
||||||
|
|
||||||
@ -41,6 +42,10 @@ const messages = defineMessages({
|
|||||||
id: 'quote_error.unauthorized',
|
id: 'quote_error.unauthorized',
|
||||||
defaultMessage: 'You are not authorized to quote this post.',
|
defaultMessage: 'You are not authorized to quote this post.',
|
||||||
},
|
},
|
||||||
|
quoteErrorPrivateMention: {
|
||||||
|
id: 'quote_error.private_mentions',
|
||||||
|
defaultMessage: 'Quoting is not allowed with direct mentions.',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
|
type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & {
|
||||||
@ -67,6 +72,39 @@ const simulateModifiedApiResponse = (
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const changeComposeVisibility = createAppThunk(
|
||||||
|
'compose/visibility_change',
|
||||||
|
(visibility: StatusVisibility, { dispatch, getState }) => {
|
||||||
|
if (visibility !== 'direct') {
|
||||||
|
return visibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = getState();
|
||||||
|
const quotedStatusId = state.compose.get('quoted_status_id') as
|
||||||
|
| string
|
||||||
|
| null;
|
||||||
|
if (!quotedStatusId) {
|
||||||
|
return visibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the quoted status
|
||||||
|
dispatch(quoteComposeCancel());
|
||||||
|
const quotedStatus = state.statuses.get(quotedStatusId) as Status | null;
|
||||||
|
if (!quotedStatus) {
|
||||||
|
return visibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the quoted status URL to the compose text
|
||||||
|
const url = quotedStatus.get('url') as string;
|
||||||
|
const text = state.compose.get('text') as string;
|
||||||
|
if (!text.includes(url)) {
|
||||||
|
const newText = text.trim() ? `${text}\n\n${url}` : url;
|
||||||
|
dispatch(changeCompose(newText));
|
||||||
|
}
|
||||||
|
return visibility;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export const changeUploadCompose = createDataLoadingThunk(
|
export const changeUploadCompose = createDataLoadingThunk(
|
||||||
'compose/changeUpload',
|
'compose/changeUpload',
|
||||||
async (
|
async (
|
||||||
@ -130,6 +168,8 @@ export const quoteComposeByStatus = createAppThunk(
|
|||||||
|
|
||||||
if (composeState.get('id')) {
|
if (composeState.get('id')) {
|
||||||
dispatch(showAlert({ message: messages.quoteErrorEdit }));
|
dispatch(showAlert({ message: messages.quoteErrorEdit }));
|
||||||
|
} else if (composeState.get('privacy') === 'direct') {
|
||||||
|
dispatch(showAlert({ message: messages.quoteErrorPrivateMention }));
|
||||||
} else if (composeState.get('poll')) {
|
} else if (composeState.get('poll')) {
|
||||||
dispatch(showAlert({ message: messages.quoteErrorPoll }));
|
dispatch(showAlert({ message: messages.quoteErrorPoll }));
|
||||||
} else if (
|
} else if (
|
||||||
@ -173,6 +213,17 @@ export const quoteComposeById = createAppThunk(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const composeStateForbidsLink = (composeState: RootState['compose']) => {
|
||||||
|
return (
|
||||||
|
composeState.get('quoted_status_id') ||
|
||||||
|
composeState.get('is_submitting') ||
|
||||||
|
composeState.get('poll') ||
|
||||||
|
composeState.get('is_uploading') ||
|
||||||
|
composeState.get('id') ||
|
||||||
|
composeState.get('privacy') === 'direct'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const pasteLinkCompose = createDataLoadingThunk(
|
export const pasteLinkCompose = createDataLoadingThunk(
|
||||||
'compose/pasteLink',
|
'compose/pasteLink',
|
||||||
async ({ url }: { url: string }) => {
|
async ({ url }: { url: string }) => {
|
||||||
@ -183,16 +234,12 @@ export const pasteLinkCompose = createDataLoadingThunk(
|
|||||||
limit: 2,
|
limit: 2,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
(data, { dispatch, getState }) => {
|
(data, { dispatch, getState, requestId }) => {
|
||||||
const composeState = getState().compose;
|
const composeState = getState().compose;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
composeState.get('quoted_status_id') ||
|
composeStateForbidsLink(composeState) ||
|
||||||
composeState.get('is_submitting') ||
|
composeState.get('fetching_link') !== requestId // Request has been cancelled
|
||||||
composeState.get('poll') ||
|
|
||||||
composeState.get('is_uploading') ||
|
|
||||||
composeState.get('id') ||
|
|
||||||
composeState.get('privacy') === 'direct'
|
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -208,6 +255,17 @@ export const pasteLinkCompose = createDataLoadingThunk(
|
|||||||
dispatch(quoteComposeById(data.statuses[0].id));
|
dispatch(quoteComposeById(data.statuses[0].id));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
useLoadingBar: false,
|
||||||
|
condition: (_, { getState }) =>
|
||||||
|
!getState().compose.get('fetching_link') &&
|
||||||
|
!composeStateForbidsLink(getState().compose),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ideally this would cancel the action and the HTTP request, but this is good enough
|
||||||
|
export const cancelPasteLinkCompose = createAction(
|
||||||
|
'compose/cancelPasteLinkCompose',
|
||||||
);
|
);
|
||||||
|
|
||||||
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
|
export const quoteComposeCancel = createAction('compose/quoteComposeCancel');
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import ModalRoot from 'mastodon/components/modal_root';
|
|||||||
import { Poll } from 'mastodon/components/poll';
|
import { Poll } from 'mastodon/components/poll';
|
||||||
import { Audio } from 'mastodon/features/audio';
|
import { Audio } from 'mastodon/features/audio';
|
||||||
import Card from 'mastodon/features/status/components/card';
|
import Card from 'mastodon/features/status/components/card';
|
||||||
import MediaModal from 'mastodon/features/ui/components/media_modal';
|
import { MediaModal } from 'mastodon/features/ui/components/media_modal';
|
||||||
import { Video } from 'mastodon/features/video';
|
import { Video } from 'mastodon/features/video';
|
||||||
import { IntlProvider } from 'mastodon/locales';
|
import { IntlProvider } from 'mastodon/locales';
|
||||||
import { createPollFromServerJSON } from 'mastodon/models/poll';
|
import { createPollFromServerJSON } from 'mastodon/models/poll';
|
||||||
|
|||||||
@ -140,7 +140,10 @@ class ComposeForm extends ImmutablePureComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.props.onSubmit(missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct');
|
this.props.onSubmit({
|
||||||
|
missingAltText: missingAltTextModal && this.props.missingAltText && this.props.privacy !== 'direct',
|
||||||
|
quoteToPrivate: this.props.quoteToPrivate,
|
||||||
|
});
|
||||||
|
|
||||||
if (e) {
|
if (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|||||||
@ -0,0 +1,48 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import type { FC } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { cancelPasteLinkCompose } from '@/mastodon/actions/compose_typed';
|
||||||
|
import { useAppDispatch } from '@/mastodon/store';
|
||||||
|
import CancelFillIcon from '@/material-icons/400-24px/cancel-fill.svg?react';
|
||||||
|
import { DisplayName } from 'mastodon/components/display_name';
|
||||||
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import { Skeleton } from 'mastodon/components/skeleton';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
quote_cancel: { id: 'status.quote.cancel', defaultMessage: 'Cancel quote' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const QuotePlaceholder: FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handleQuoteCancel = useCallback(() => {
|
||||||
|
dispatch(cancelPasteLinkCompose());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='status__quote'>
|
||||||
|
<div className='status'>
|
||||||
|
<div className='status__info'>
|
||||||
|
<div className='status__avatar'>
|
||||||
|
<Skeleton width='32px' height='32px' />
|
||||||
|
</div>
|
||||||
|
<div className='status__display-name'>
|
||||||
|
<DisplayName />
|
||||||
|
</div>
|
||||||
|
<IconButton
|
||||||
|
onClick={handleQuoteCancel}
|
||||||
|
className='status__quote-cancel'
|
||||||
|
title={intl.formatMessage(messages.quote_cancel)}
|
||||||
|
icon='cancel-fill'
|
||||||
|
iconComponent={CancelFillIcon}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='status__content'>
|
||||||
|
<Skeleton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -7,11 +7,17 @@ import { quoteComposeCancel } from '@/mastodon/actions/compose_typed';
|
|||||||
import { QuotedStatus } from '@/mastodon/components/status_quoted';
|
import { QuotedStatus } from '@/mastodon/components/status_quoted';
|
||||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
|
|
||||||
|
import { QuotePlaceholder } from './quote_placeholder';
|
||||||
|
|
||||||
export const ComposeQuotedStatus: FC = () => {
|
export const ComposeQuotedStatus: FC = () => {
|
||||||
const quotedStatusId = useAppSelector(
|
const quotedStatusId = useAppSelector(
|
||||||
(state) => state.compose.get('quoted_status_id') as string | null,
|
(state) => state.compose.get('quoted_status_id') as string | null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isFetchingLink = useAppSelector(
|
||||||
|
(state) => !!state.compose.get('fetching_link'),
|
||||||
|
);
|
||||||
|
|
||||||
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
|
const isEditing = useAppSelector((state) => !!state.compose.get('id'));
|
||||||
|
|
||||||
const quote = useMemo(
|
const quote = useMemo(
|
||||||
@ -30,7 +36,9 @@ export const ComposeQuotedStatus: FC = () => {
|
|||||||
dispatch(quoteComposeCancel());
|
dispatch(quoteComposeCancel());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
if (!quote) {
|
if (isFetchingLink && !quote) {
|
||||||
|
return <QuotePlaceholder />;
|
||||||
|
} else if (!quote) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,10 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { changeComposeVisibility } from '@/mastodon/actions/compose';
|
import {
|
||||||
import { setComposeQuotePolicy } from '@/mastodon/actions/compose_typed';
|
changeComposeVisibility,
|
||||||
|
setComposeQuotePolicy,
|
||||||
|
} from '@/mastodon/actions/compose_typed';
|
||||||
import { openModal } from '@/mastodon/actions/modal';
|
import { openModal } from '@/mastodon/actions/modal';
|
||||||
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
|
import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
|
||||||
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
} from 'mastodon/actions/compose';
|
} from 'mastodon/actions/compose';
|
||||||
import { pasteLinkCompose } from 'mastodon/actions/compose_typed';
|
import { pasteLinkCompose } from 'mastodon/actions/compose_typed';
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { PRIVATE_QUOTE_MODAL_ID } from 'mastodon/features/ui/components/confirmation_modals/private_quote_notify';
|
||||||
|
|
||||||
import ComposeForm from '../components/compose_form';
|
import ComposeForm from '../components/compose_form';
|
||||||
|
|
||||||
@ -32,6 +33,10 @@ const mapStateToProps = state => ({
|
|||||||
isUploading: state.getIn(['compose', 'is_uploading']),
|
isUploading: state.getIn(['compose', 'is_uploading']),
|
||||||
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||||
missingAltText: state.getIn(['compose', 'media_attachments']).some(media => ['image', 'gifv'].includes(media.get('type')) && (media.get('description') ?? '').length === 0),
|
missingAltText: state.getIn(['compose', 'media_attachments']).some(media => ['image', 'gifv'].includes(media.get('type')) && (media.get('description') ?? '').length === 0),
|
||||||
|
quoteToPrivate:
|
||||||
|
!!state.getIn(['compose', 'quoted_status_id'])
|
||||||
|
&& state.getIn(['compose', 'privacy']) === 'private'
|
||||||
|
&& !state.getIn(['settings', 'dismissed_banners', PRIVATE_QUOTE_MODAL_ID]),
|
||||||
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
|
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
|
||||||
lang: state.getIn(['compose', 'language']),
|
lang: state.getIn(['compose', 'language']),
|
||||||
maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500),
|
maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500),
|
||||||
@ -43,12 +48,17 @@ const mapDispatchToProps = (dispatch, props) => ({
|
|||||||
dispatch(changeCompose(text));
|
dispatch(changeCompose(text));
|
||||||
},
|
},
|
||||||
|
|
||||||
onSubmit (missingAltText) {
|
onSubmit ({ missingAltText, quoteToPrivate }) {
|
||||||
if (missingAltText) {
|
if (missingAltText) {
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalType: 'CONFIRM_MISSING_ALT_TEXT',
|
modalType: 'CONFIRM_MISSING_ALT_TEXT',
|
||||||
modalProps: {},
|
modalProps: {},
|
||||||
}));
|
}));
|
||||||
|
} else if (quoteToPrivate) {
|
||||||
|
dispatch(openModal({
|
||||||
|
modalType: 'CONFIRM_PRIVATE_QUOTE_NOTIFY',
|
||||||
|
modalProps: {},
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
dispatch(submitCompose((status) => {
|
dispatch(submitCompose((status) => {
|
||||||
if (props.redirectOnSuccess) {
|
if (props.redirectOnSuccess) {
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { changeComposeVisibility } from '../../../actions/compose';
|
import { changeComposeVisibility } from '@/mastodon/actions/compose_typed';
|
||||||
import { openModal, closeModal } from '../../../actions/modal';
|
|
||||||
import { isUserTouching } from '../../../is_mobile';
|
|
||||||
import PrivacyDropdown from '../components/privacy_dropdown';
|
import PrivacyDropdown from '../components/privacy_dropdown';
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
|
|||||||
@ -299,6 +299,12 @@ class Status extends ImmutablePureComponent {
|
|||||||
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId, onChange: handleChange } }));
|
dispatch(openModal({ modalType: 'COMPOSE_PRIVACY', modalProps: { statusId, onChange: handleChange } }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleQuote = (status) => {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
|
dispatch(quoteComposeById(status.get('id')));
|
||||||
|
};
|
||||||
|
|
||||||
handleEditClick = (status) => {
|
handleEditClick = (status) => {
|
||||||
const { dispatch, askReplyConfirmation } = this.props;
|
const { dispatch, askReplyConfirmation } = this.props;
|
||||||
|
|
||||||
@ -592,7 +598,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
showBackButton
|
showBackButton
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
extraButton={(
|
extraButton={(
|
||||||
<button type='button' className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} icon={status.get('hidden') ? VisibilityOffIcon : VisibilityIcon} /></button>
|
<button type='button' className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll}><Icon id={status.get('hidden') ? 'eye' : 'eye-slash'} icon={status.get('hidden') ? VisibilityIcon : VisibilityOffIcon} /></button>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -625,6 +631,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
onDelete={this.handleDeleteClick}
|
onDelete={this.handleDeleteClick}
|
||||||
onRevokeQuote={this.handleRevokeQuoteClick}
|
onRevokeQuote={this.handleRevokeQuoteClick}
|
||||||
onQuotePolicyChange={this.handleQuotePolicyChange}
|
onQuotePolicyChange={this.handleQuotePolicyChange}
|
||||||
|
onQuote={this.handleQuote}
|
||||||
onEdit={this.handleEditClick}
|
onEdit={this.handleEditClick}
|
||||||
onDirect={this.handleDirectClick}
|
onDirect={this.handleDirectClick}
|
||||||
onMention={this.handleMentionClick}
|
onMention={this.handleMentionClick}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export const ConfirmationModal: React.FC<
|
|||||||
onSecondary?: () => void;
|
onSecondary?: () => void;
|
||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
closeWhenConfirm?: boolean;
|
closeWhenConfirm?: boolean;
|
||||||
|
extraContent?: React.ReactNode;
|
||||||
} & BaseConfirmationModalProps
|
} & BaseConfirmationModalProps
|
||||||
> = ({
|
> = ({
|
||||||
title,
|
title,
|
||||||
@ -29,6 +30,7 @@ export const ConfirmationModal: React.FC<
|
|||||||
secondary,
|
secondary,
|
||||||
onSecondary,
|
onSecondary,
|
||||||
closeWhenConfirm = true,
|
closeWhenConfirm = true,
|
||||||
|
extraContent,
|
||||||
}) => {
|
}) => {
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (closeWhenConfirm) {
|
if (closeWhenConfirm) {
|
||||||
@ -49,6 +51,8 @@ export const ConfirmationModal: React.FC<
|
|||||||
<div className='safety-action-modal__confirmation'>
|
<div className='safety-action-modal__confirmation'>
|
||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
{message && <p>{message}</p>}
|
{message && <p>{message}</p>}
|
||||||
|
|
||||||
|
{extraContent}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,88 @@
|
|||||||
|
import { forwardRef, useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { submitCompose } from '@/mastodon/actions/compose';
|
||||||
|
import { changeSetting } from '@/mastodon/actions/settings';
|
||||||
|
import { CheckBox } from '@/mastodon/components/check_box';
|
||||||
|
import { useAppDispatch } from '@/mastodon/store';
|
||||||
|
|
||||||
|
import { ConfirmationModal } from './confirmation_modal';
|
||||||
|
import type { BaseConfirmationModalProps } from './confirmation_modal';
|
||||||
|
import classes from './styles.module.css';
|
||||||
|
|
||||||
|
export const PRIVATE_QUOTE_MODAL_ID = 'quote/private_notify';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: {
|
||||||
|
id: 'confirmations.private_quote_notify.title',
|
||||||
|
defaultMessage: 'Share with followers and mentioned users?',
|
||||||
|
},
|
||||||
|
message: {
|
||||||
|
id: 'confirmations.private_quote_notify.message',
|
||||||
|
defaultMessage:
|
||||||
|
'The person you are quoting and other mentions ' +
|
||||||
|
"will be notified and will be able to view your post, even if they're not following you.",
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
id: 'confirmations.private_quote_notify.confirm',
|
||||||
|
defaultMessage: 'Publish post',
|
||||||
|
},
|
||||||
|
cancel: {
|
||||||
|
id: 'confirmations.private_quote_notify.cancel',
|
||||||
|
defaultMessage: 'Back to editing',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PrivateQuoteNotify = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
BaseConfirmationModalProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{ onClose },
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
_ref,
|
||||||
|
) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const [dismiss, setDismissed] = useState(false);
|
||||||
|
const handleDismissToggle = useCallback(() => {
|
||||||
|
setDismissed((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const handleConfirm = useCallback(() => {
|
||||||
|
dispatch(submitCompose());
|
||||||
|
if (dismiss) {
|
||||||
|
dispatch(
|
||||||
|
changeSetting(['dismissed_banners', PRIVATE_QUOTE_MODAL_ID], true),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [dismiss, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmationModal
|
||||||
|
title={intl.formatMessage(messages.title)}
|
||||||
|
message={intl.formatMessage(messages.message)}
|
||||||
|
confirm={intl.formatMessage(messages.confirm)}
|
||||||
|
cancel={intl.formatMessage(messages.cancel)}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
onClose={onClose}
|
||||||
|
extraContent={
|
||||||
|
<label className={classes.checkbox_wrapper}>
|
||||||
|
<CheckBox
|
||||||
|
value='hide'
|
||||||
|
checked={dismiss}
|
||||||
|
onChange={handleDismissToggle}
|
||||||
|
/>{' '}
|
||||||
|
<FormattedMessage
|
||||||
|
id='confirmations.private_quote_notify.do_not_show_again'
|
||||||
|
defaultMessage="Don't show me this message again"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
PrivateQuoteNotify.displayName = 'PrivateQuoteNotify';
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
.checkbox_wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@ -1,296 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import ReactSwipeableViews from 'react-swipeable-views';
|
|
||||||
|
|
||||||
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
|
||||||
import FitScreenIcon from '@/material-icons/400-24px/fit_screen.svg?react';
|
|
||||||
import ActualSizeIcon from '@/svg-icons/actual_size.svg?react';
|
|
||||||
import { getAverageFromBlurhash } from 'mastodon/blurhash';
|
|
||||||
import { GIFV } from 'mastodon/components/gifv';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
|
||||||
import { Footer } from 'mastodon/features/picture_in_picture/components/footer';
|
|
||||||
import { Video } from 'mastodon/features/video';
|
|
||||||
import { disableSwiping } from 'mastodon/initial_state';
|
|
||||||
|
|
||||||
import { ZoomableImage } from './zoomable_image';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
|
||||||
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
|
|
||||||
next: { id: 'lightbox.next', defaultMessage: 'Next' },
|
|
||||||
zoomIn: { id: 'lightbox.zoom_in', defaultMessage: 'Zoom to actual size' },
|
|
||||||
zoomOut: { id: 'lightbox.zoom_out', defaultMessage: 'Zoom to fit' },
|
|
||||||
});
|
|
||||||
|
|
||||||
class MediaModal extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
media: ImmutablePropTypes.list.isRequired,
|
|
||||||
statusId: PropTypes.string,
|
|
||||||
lang: PropTypes.string,
|
|
||||||
index: PropTypes.number.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
onChangeBackgroundColor: PropTypes.func.isRequired,
|
|
||||||
currentTime: PropTypes.number,
|
|
||||||
autoPlay: PropTypes.bool,
|
|
||||||
volume: PropTypes.number,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
index: null,
|
|
||||||
navigationHidden: false,
|
|
||||||
zoomedIn: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleZoomClick = () => {
|
|
||||||
this.setState(prevState => ({
|
|
||||||
zoomedIn: !prevState.zoomedIn,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
handleZoomChange = (zoomedIn) => {
|
|
||||||
this.setState({
|
|
||||||
zoomedIn,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSwipe = (index) => {
|
|
||||||
this.setState({
|
|
||||||
index: index % this.props.media.size,
|
|
||||||
zoomedIn: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleTransitionEnd = () => {
|
|
||||||
this.setState({
|
|
||||||
zoomedIn: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleNextClick = () => {
|
|
||||||
this.setState({
|
|
||||||
index: (this.getIndex() + 1) % this.props.media.size,
|
|
||||||
zoomedIn: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handlePrevClick = () => {
|
|
||||||
this.setState({
|
|
||||||
index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size,
|
|
||||||
zoomedIn: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleChangeIndex = (e) => {
|
|
||||||
const index = Number(e.currentTarget.getAttribute('data-index'));
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
index: index % this.props.media.size,
|
|
||||||
zoomedIn: false,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
handleKeyDown = (e) => {
|
|
||||||
switch(e.key) {
|
|
||||||
case 'ArrowLeft':
|
|
||||||
this.handlePrevClick();
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
break;
|
|
||||||
case 'ArrowRight':
|
|
||||||
this.handleNextClick();
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
window.addEventListener('keydown', this.handleKeyDown, false);
|
|
||||||
|
|
||||||
this._sendBackgroundColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate (prevProps, prevState) {
|
|
||||||
if (prevState.index !== this.state.index) {
|
|
||||||
this._sendBackgroundColor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_sendBackgroundColor () {
|
|
||||||
const { media, onChangeBackgroundColor } = this.props;
|
|
||||||
const index = this.getIndex();
|
|
||||||
const blurhash = media.getIn([index, 'blurhash']);
|
|
||||||
|
|
||||||
if (blurhash) {
|
|
||||||
const backgroundColor = getAverageFromBlurhash(blurhash);
|
|
||||||
onChangeBackgroundColor(backgroundColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
window.removeEventListener('keydown', this.handleKeyDown);
|
|
||||||
|
|
||||||
this.props.onChangeBackgroundColor(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
getIndex () {
|
|
||||||
return this.state.index !== null ? this.state.index : this.props.index;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleToggleNavigation = () => {
|
|
||||||
this.setState(prevState => ({
|
|
||||||
navigationHidden: !prevState.navigationHidden,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
setRef = c => {
|
|
||||||
this.setState({
|
|
||||||
viewportWidth: c?.clientWidth,
|
|
||||||
viewportHeight: c?.clientHeight,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { media, statusId, lang, intl, onClose } = this.props;
|
|
||||||
const { navigationHidden, zoomedIn, viewportWidth, viewportHeight } = this.state;
|
|
||||||
|
|
||||||
const index = this.getIndex();
|
|
||||||
|
|
||||||
const leftNav = media.size > 1 && <button tabIndex={0} className='media-modal__nav media-modal__nav--prev' onClick={this.handlePrevClick} aria-label={intl.formatMessage(messages.previous)}><Icon id='chevron-left' icon={ChevronLeftIcon} /></button>;
|
|
||||||
const rightNav = media.size > 1 && <button tabIndex={0} className='media-modal__nav media-modal__nav--next' onClick={this.handleNextClick} aria-label={intl.formatMessage(messages.next)}><Icon id='chevron-right' icon={ChevronRightIcon} /></button>;
|
|
||||||
|
|
||||||
const content = media.map((image, idx) => {
|
|
||||||
const width = image.getIn(['meta', 'original', 'width']) || null;
|
|
||||||
const height = image.getIn(['meta', 'original', 'height']) || null;
|
|
||||||
const description = image.getIn(['translation', 'description']) || image.get('description');
|
|
||||||
|
|
||||||
if (image.get('type') === 'image') {
|
|
||||||
return (
|
|
||||||
<ZoomableImage
|
|
||||||
src={image.get('url')}
|
|
||||||
blurhash={image.get('blurhash')}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
alt={description}
|
|
||||||
lang={lang}
|
|
||||||
key={image.get('url')}
|
|
||||||
onClick={this.handleToggleNavigation}
|
|
||||||
onDoubleClick={this.handleZoomClick}
|
|
||||||
onClose={onClose}
|
|
||||||
onZoomChange={this.handleZoomChange}
|
|
||||||
zoomedIn={zoomedIn && idx === index}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (image.get('type') === 'video') {
|
|
||||||
const { currentTime, autoPlay, volume } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Video
|
|
||||||
preview={image.get('preview_url')}
|
|
||||||
blurhash={image.get('blurhash')}
|
|
||||||
src={image.get('url')}
|
|
||||||
frameRate={image.getIn(['meta', 'original', 'frame_rate'])}
|
|
||||||
aspectRatio={`${image.getIn(['meta', 'original', 'width'])} / ${image.getIn(['meta', 'original', 'height'])}`}
|
|
||||||
startTime={currentTime || 0}
|
|
||||||
startPlaying={autoPlay || false}
|
|
||||||
startVolume={volume || 1}
|
|
||||||
onCloseVideo={onClose}
|
|
||||||
detailed
|
|
||||||
alt={description}
|
|
||||||
lang={lang}
|
|
||||||
key={image.get('url')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (image.get('type') === 'gifv') {
|
|
||||||
return (
|
|
||||||
<GIFV
|
|
||||||
src={image.get('url')}
|
|
||||||
key={image.get('url')}
|
|
||||||
alt={description}
|
|
||||||
lang={lang}
|
|
||||||
onClick={this.toggleNavigation}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}).toArray();
|
|
||||||
|
|
||||||
// you can't use 100vh, because the viewport height is taller
|
|
||||||
// than the visible part of the document in some mobile
|
|
||||||
// browsers when it's address bar is visible.
|
|
||||||
// https://developers.google.com/web/updates/2016/12/url-bar-resizing
|
|
||||||
const swipeableViewsStyle = {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
};
|
|
||||||
|
|
||||||
const containerStyle = {
|
|
||||||
alignItems: 'center', // center vertically
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigationClassName = classNames('media-modal__navigation', {
|
|
||||||
'media-modal__navigation--hidden': navigationHidden,
|
|
||||||
});
|
|
||||||
|
|
||||||
let pagination;
|
|
||||||
|
|
||||||
if (media.size > 1) {
|
|
||||||
pagination = media.map((item, i) => (
|
|
||||||
<button key={i} className={classNames('media-modal__page-dot', { active: i === index })} data-index={i} onClick={this.handleChangeIndex}>
|
|
||||||
{i + 1}
|
|
||||||
</button>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentMedia = media.get(index);
|
|
||||||
const zoomable = currentMedia.get('type') === 'image' && (currentMedia.getIn(['meta', 'original', 'width']) > viewportWidth || currentMedia.getIn(['meta', 'original', 'height']) > viewportHeight);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='modal-root__modal media-modal' ref={this.setRef}>
|
|
||||||
<div className='media-modal__closer' role='presentation' onClick={onClose}>
|
|
||||||
<ReactSwipeableViews
|
|
||||||
style={swipeableViewsStyle}
|
|
||||||
containerStyle={containerStyle}
|
|
||||||
onChangeIndex={this.handleSwipe}
|
|
||||||
onTransitionEnd={this.handleTransitionEnd}
|
|
||||||
index={index}
|
|
||||||
disabled={disableSwiping || zoomedIn}
|
|
||||||
>
|
|
||||||
{content}
|
|
||||||
</ReactSwipeableViews>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={navigationClassName}>
|
|
||||||
<div className='media-modal__buttons'>
|
|
||||||
{zoomable && <IconButton title={intl.formatMessage(zoomedIn ? messages.zoomOut : messages.zoomIn)} iconComponent={zoomedIn ? FitScreenIcon : ActualSizeIcon} onClick={this.handleZoomClick} />}
|
|
||||||
<IconButton title={intl.formatMessage(messages.close)} icon='times' iconComponent={CloseIcon} onClick={onClose} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{leftNav}
|
|
||||||
{rightNav}
|
|
||||||
|
|
||||||
<div className='media-modal__overlay'>
|
|
||||||
{pagination && <ul className='media-modal__pagination'>{pagination}</ul>}
|
|
||||||
{statusId && <Footer statusId={statusId} withOpenButton onClose={onClose} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(MediaModal);
|
|
||||||
363
app/javascript/mastodon/features/ui/components/media_modal.tsx
Normal file
363
app/javascript/mastodon/features/ui/components/media_modal.tsx
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import type { RefCallback, FC } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import type { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
|
import { animated, useSpring } from '@react-spring/web';
|
||||||
|
import { useDrag } from '@use-gesture/react';
|
||||||
|
|
||||||
|
import type { MediaAttachment } from '@/mastodon/models/status';
|
||||||
|
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||||
|
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
|
import FitScreenIcon from '@/material-icons/400-24px/fit_screen.svg?react';
|
||||||
|
import ActualSizeIcon from '@/svg-icons/actual_size.svg?react';
|
||||||
|
import type { RGB } from 'mastodon/blurhash';
|
||||||
|
import { getAverageFromBlurhash } from 'mastodon/blurhash';
|
||||||
|
import { GIFV } from 'mastodon/components/gifv';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import { Footer } from 'mastodon/features/picture_in_picture/components/footer';
|
||||||
|
import { Video } from 'mastodon/features/video';
|
||||||
|
|
||||||
|
import { ZoomableImage } from './zoomable_image';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
|
previous: { id: 'lightbox.previous', defaultMessage: 'Previous' },
|
||||||
|
next: { id: 'lightbox.next', defaultMessage: 'Next' },
|
||||||
|
zoomIn: { id: 'lightbox.zoom_in', defaultMessage: 'Zoom to actual size' },
|
||||||
|
zoomOut: { id: 'lightbox.zoom_out', defaultMessage: 'Zoom to fit' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface MediaModalProps {
|
||||||
|
media: ImmutableList<MediaAttachment>;
|
||||||
|
statusId?: string;
|
||||||
|
lang?: string;
|
||||||
|
index: number;
|
||||||
|
onClose: () => void;
|
||||||
|
onChangeBackgroundColor: (color: RGB | null) => void;
|
||||||
|
currentTime?: number;
|
||||||
|
autoPlay?: boolean;
|
||||||
|
volume?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MediaModal: FC<MediaModalProps> = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
MediaModalProps
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
media,
|
||||||
|
onClose,
|
||||||
|
index: startIndex,
|
||||||
|
lang,
|
||||||
|
currentTime,
|
||||||
|
autoPlay,
|
||||||
|
volume,
|
||||||
|
statusId,
|
||||||
|
onChangeBackgroundColor,
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- _ref is required to keep the ref forwarding working
|
||||||
|
_ref,
|
||||||
|
) => {
|
||||||
|
const [index, setIndex] = useState(startIndex);
|
||||||
|
const currentMedia = media.get(index);
|
||||||
|
|
||||||
|
const handleChangeIndex = useCallback(
|
||||||
|
(newIndex: number) => {
|
||||||
|
if (newIndex < 0) {
|
||||||
|
newIndex = media.size + newIndex;
|
||||||
|
}
|
||||||
|
setIndex(newIndex % media.size);
|
||||||
|
setZoomedIn(false);
|
||||||
|
},
|
||||||
|
[media.size],
|
||||||
|
);
|
||||||
|
const handlePrevClick = useCallback(() => {
|
||||||
|
handleChangeIndex(index - 1);
|
||||||
|
}, [handleChangeIndex, index]);
|
||||||
|
const handleNextClick = useCallback(() => {
|
||||||
|
handleChangeIndex(index + 1);
|
||||||
|
}, [handleChangeIndex, index]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'ArrowLeft') {
|
||||||
|
handlePrevClick();
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
} else if (event.key === 'ArrowRight') {
|
||||||
|
handleNextClick();
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleNextClick, handlePrevClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('keydown', handleKeyDown, false);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [handleKeyDown]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const blurhash = currentMedia?.get('blurhash') as string | undefined;
|
||||||
|
if (blurhash) {
|
||||||
|
const backgroundColor = getAverageFromBlurhash(blurhash);
|
||||||
|
if (backgroundColor) {
|
||||||
|
onChangeBackgroundColor(backgroundColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [currentMedia, onChangeBackgroundColor]);
|
||||||
|
|
||||||
|
const [viewportDimensions, setViewportDimensions] = useState<{
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}>({ width: 0, height: 0 });
|
||||||
|
const handleRef: RefCallback<HTMLDivElement> = useCallback((ele) => {
|
||||||
|
if (ele?.clientWidth && ele.clientHeight) {
|
||||||
|
setViewportDimensions({
|
||||||
|
width: ele.clientWidth,
|
||||||
|
height: ele.clientHeight,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [zoomedIn, setZoomedIn] = useState(false);
|
||||||
|
const zoomable =
|
||||||
|
currentMedia?.get('type') === 'image' &&
|
||||||
|
((currentMedia.getIn(['meta', 'original', 'width']) as number) >
|
||||||
|
viewportDimensions.width ||
|
||||||
|
(currentMedia.getIn(['meta', 'original', 'height']) as number) >
|
||||||
|
viewportDimensions.height);
|
||||||
|
const handleZoomClick = useCallback(() => {
|
||||||
|
setZoomedIn((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const wrapperStyles = useSpring({
|
||||||
|
x: `-${index * 100}%`,
|
||||||
|
});
|
||||||
|
const bind = useDrag(
|
||||||
|
({ swipe: [swipeX] }) => {
|
||||||
|
if (swipeX === 0) return;
|
||||||
|
handleChangeIndex(index + swipeX * -1); // Invert swipe as swiping left loads the next slide.
|
||||||
|
},
|
||||||
|
{ pointer: { capture: false } },
|
||||||
|
);
|
||||||
|
|
||||||
|
const [navigationHidden, setNavigationHidden] = useState(false);
|
||||||
|
const handleToggleNavigation = useCallback(() => {
|
||||||
|
setNavigationHidden((prev) => !prev);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const content = useMemo(
|
||||||
|
() =>
|
||||||
|
media.map((item, idx) => {
|
||||||
|
const url = item.get('url') as string;
|
||||||
|
const blurhash = item.get('blurhash') as string;
|
||||||
|
const width = item.getIn(['meta', 'original', 'width'], 0) as number;
|
||||||
|
const height = item.getIn(
|
||||||
|
['meta', 'original', 'height'],
|
||||||
|
0,
|
||||||
|
) as number;
|
||||||
|
const description = item.getIn(
|
||||||
|
['translation', 'description'],
|
||||||
|
item.get('description'),
|
||||||
|
) as string;
|
||||||
|
if (item.get('type') === 'image') {
|
||||||
|
return (
|
||||||
|
<ZoomableImage
|
||||||
|
src={url}
|
||||||
|
blurhash={blurhash}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
alt={description}
|
||||||
|
lang={lang}
|
||||||
|
key={url}
|
||||||
|
onClick={handleToggleNavigation}
|
||||||
|
onDoubleClick={handleZoomClick}
|
||||||
|
onClose={onClose}
|
||||||
|
onZoomChange={setZoomedIn}
|
||||||
|
zoomedIn={zoomedIn && idx === index}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (item.get('type') === 'video') {
|
||||||
|
return (
|
||||||
|
<Video
|
||||||
|
preview={item.get('preview_url') as string | undefined}
|
||||||
|
blurhash={blurhash}
|
||||||
|
src={url}
|
||||||
|
frameRate={
|
||||||
|
item.getIn(['meta', 'original', 'frame_rate']) as
|
||||||
|
| string
|
||||||
|
| undefined
|
||||||
|
}
|
||||||
|
aspectRatio={`${width} / ${height}`}
|
||||||
|
startTime={currentTime ?? 0}
|
||||||
|
startPlaying={autoPlay ?? false}
|
||||||
|
startVolume={volume ?? 1}
|
||||||
|
onCloseVideo={onClose}
|
||||||
|
detailed
|
||||||
|
alt={description}
|
||||||
|
lang={lang}
|
||||||
|
key={url}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (item.get('type') === 'gifv') {
|
||||||
|
return (
|
||||||
|
<GIFV
|
||||||
|
src={url}
|
||||||
|
key={url}
|
||||||
|
alt={description}
|
||||||
|
lang={lang}
|
||||||
|
onClick={handleToggleNavigation}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
autoPlay,
|
||||||
|
currentTime,
|
||||||
|
handleToggleNavigation,
|
||||||
|
handleZoomClick,
|
||||||
|
index,
|
||||||
|
lang,
|
||||||
|
media,
|
||||||
|
onClose,
|
||||||
|
volume,
|
||||||
|
zoomedIn,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const leftNav = media.size > 1 && (
|
||||||
|
<button
|
||||||
|
tabIndex={0}
|
||||||
|
className='media-modal__nav media-modal__nav--prev'
|
||||||
|
onClick={handlePrevClick}
|
||||||
|
aria-label={intl.formatMessage(messages.previous)}
|
||||||
|
>
|
||||||
|
<Icon id='chevron-left' icon={ChevronLeftIcon} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
const rightNav = media.size > 1 && (
|
||||||
|
<button
|
||||||
|
tabIndex={0}
|
||||||
|
className='media-modal__nav media-modal__nav--next'
|
||||||
|
onClick={handleNextClick}
|
||||||
|
aria-label={intl.formatMessage(messages.next)}
|
||||||
|
>
|
||||||
|
<Icon id='chevron-right' icon={ChevronRightIcon} />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...bind()}
|
||||||
|
className='modal-root__modal media-modal'
|
||||||
|
ref={handleRef}
|
||||||
|
>
|
||||||
|
<animated.div
|
||||||
|
style={wrapperStyles}
|
||||||
|
className='media-modal__closer'
|
||||||
|
role='presentation'
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</animated.div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames('media-modal__navigation', {
|
||||||
|
'media-modal__navigation--hidden': navigationHidden,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className='media-modal__buttons'>
|
||||||
|
{zoomable && (
|
||||||
|
<IconButton
|
||||||
|
title={intl.formatMessage(
|
||||||
|
zoomedIn ? messages.zoomOut : messages.zoomIn,
|
||||||
|
)}
|
||||||
|
icon=''
|
||||||
|
iconComponent={zoomedIn ? FitScreenIcon : ActualSizeIcon}
|
||||||
|
onClick={handleZoomClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
title={intl.formatMessage(messages.close)}
|
||||||
|
icon='times'
|
||||||
|
iconComponent={CloseIcon}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{leftNav}
|
||||||
|
{rightNav}
|
||||||
|
|
||||||
|
<div className='media-modal__overlay'>
|
||||||
|
<MediaPagination
|
||||||
|
itemsCount={media.size}
|
||||||
|
index={index}
|
||||||
|
onChangeIndex={handleChangeIndex}
|
||||||
|
/>
|
||||||
|
{statusId && (
|
||||||
|
<Footer statusId={statusId} withOpenButton onClose={onClose} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
MediaModal.displayName = 'MediaModal';
|
||||||
|
|
||||||
|
interface MediaPaginationProps {
|
||||||
|
itemsCount: number;
|
||||||
|
index: number;
|
||||||
|
onChangeIndex: (newIndex: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MediaPagination: FC<MediaPaginationProps> = ({
|
||||||
|
itemsCount,
|
||||||
|
index,
|
||||||
|
onChangeIndex,
|
||||||
|
}) => {
|
||||||
|
const handleChangeIndex = useCallback(
|
||||||
|
(curIndex: number) => {
|
||||||
|
return () => {
|
||||||
|
onChangeIndex(curIndex);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[onChangeIndex],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (itemsCount <= 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className='media-modal__pagination'>
|
||||||
|
{Array.from({ length: itemsCount }).map((_, i) => (
|
||||||
|
<button
|
||||||
|
key={i}
|
||||||
|
className={classNames('media-modal__page-dot', {
|
||||||
|
active: i === index,
|
||||||
|
})}
|
||||||
|
onClick={handleChangeIndex(i)}
|
||||||
|
>
|
||||||
|
{i + 1}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -43,10 +43,11 @@ import {
|
|||||||
QuietPostQuoteInfoModal,
|
QuietPostQuoteInfoModal,
|
||||||
} from './confirmation_modals';
|
} from './confirmation_modals';
|
||||||
import { ImageModal } from './image_modal';
|
import { ImageModal } from './image_modal';
|
||||||
import MediaModal from './media_modal';
|
import { MediaModal } from './media_modal';
|
||||||
import { ModalPlaceholder } from './modal_placeholder';
|
import { ModalPlaceholder } from './modal_placeholder';
|
||||||
import VideoModal from './video_modal';
|
import VideoModal from './video_modal';
|
||||||
import { VisibilityModal } from './visibility_modal';
|
import { VisibilityModal } from './visibility_modal';
|
||||||
|
import { PrivateQuoteNotify } from './confirmation_modals/private_quote_notify';
|
||||||
|
|
||||||
export const MODAL_COMPONENTS = {
|
export const MODAL_COMPONENTS = {
|
||||||
'MEDIA': () => Promise.resolve({ default: MediaModal }),
|
'MEDIA': () => Promise.resolve({ default: MediaModal }),
|
||||||
@ -66,6 +67,7 @@ export const MODAL_COMPONENTS = {
|
|||||||
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
|
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
|
||||||
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
|
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
|
||||||
'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
|
'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
|
||||||
|
'CONFIRM_PRIVATE_QUOTE_NOTIFY': () => Promise.resolve({ default: PrivateQuoteNotify }),
|
||||||
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),
|
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),
|
||||||
'CONFIRM_QUIET_QUOTE': () => Promise.resolve({ default: QuietPostQuoteInfoModal }),
|
'CONFIRM_QUIET_QUOTE': () => Promise.resolve({ default: QuietPostQuoteInfoModal }),
|
||||||
'MUTE': MuteModal,
|
'MUTE': MuteModal,
|
||||||
|
|||||||
@ -128,9 +128,12 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
|
|||||||
const disableVisibility = !!statusId;
|
const disableVisibility = !!statusId;
|
||||||
const disableQuotePolicy =
|
const disableQuotePolicy =
|
||||||
visibility === 'private' || visibility === 'direct';
|
visibility === 'private' || visibility === 'direct';
|
||||||
const disablePublicVisibilities: boolean = useAppSelector(
|
const disablePublicVisibilities = useAppSelector(
|
||||||
selectDisablePublicVisibilities,
|
selectDisablePublicVisibilities,
|
||||||
);
|
);
|
||||||
|
const isQuotePost = useAppSelector(
|
||||||
|
(state) => state.compose.get('quoted_status_id') !== null,
|
||||||
|
);
|
||||||
|
|
||||||
const visibilityItems = useMemo<SelectItem<StatusVisibility>[]>(() => {
|
const visibilityItems = useMemo<SelectItem<StatusVisibility>[]>(() => {
|
||||||
const items: SelectItem<StatusVisibility>[] = [
|
const items: SelectItem<StatusVisibility>[] = [
|
||||||
@ -315,6 +318,21 @@ export const VisibilityModal: FC<VisibilityModalProps> = forwardRef(
|
|||||||
id={quoteDescriptionId}
|
id={quoteDescriptionId}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isQuotePost && visibility === 'direct' && (
|
||||||
|
<div className='visibility-modal__quote-warning'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='visibility_modal.direct_quote_warning.title'
|
||||||
|
defaultMessage="Quotes can't be embedded in private mentions"
|
||||||
|
tagName='h3'
|
||||||
|
/>
|
||||||
|
<FormattedMessage
|
||||||
|
id='visibility_modal.direct_quote_warning.text'
|
||||||
|
defaultMessage='If you save the current settings, the embedded quote will be converted to a link.'
|
||||||
|
tagName='p'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='dialog-modal__content__actions'>
|
<div className='dialog-modal__content__actions'>
|
||||||
<Button onClick={onClose} secondary>
|
<Button onClick={onClose} secondary>
|
||||||
|
|||||||
@ -35,7 +35,7 @@ interface InitialStateMeta {
|
|||||||
streaming_api_base_url: string;
|
streaming_api_base_url: string;
|
||||||
local_live_feed_access: 'public' | 'authenticated' | 'disabled';
|
local_live_feed_access: 'public' | 'authenticated' | 'disabled';
|
||||||
remote_live_feed_access: 'public' | 'authenticated' | 'disabled';
|
remote_live_feed_access: 'public' | 'authenticated' | 'disabled';
|
||||||
local_topic_feed_access: 'public' | 'authenticated' | 'disabled';
|
local_topic_feed_access: 'public' | 'authenticated';
|
||||||
remote_topic_feed_access: 'public' | 'authenticated' | 'disabled';
|
remote_topic_feed_access: 'public' | 'authenticated' | 'disabled';
|
||||||
title: string;
|
title: string;
|
||||||
show_trends: boolean;
|
show_trends: boolean;
|
||||||
|
|||||||
@ -157,6 +157,8 @@
|
|||||||
"bundle_modal_error.close": "Cau",
|
"bundle_modal_error.close": "Cau",
|
||||||
"bundle_modal_error.message": "Aeth rhywbeth o'i le wrth lwytho'r sgrin hon.",
|
"bundle_modal_error.message": "Aeth rhywbeth o'i le wrth lwytho'r sgrin hon.",
|
||||||
"bundle_modal_error.retry": "Ceisiwch eto",
|
"bundle_modal_error.retry": "Ceisiwch eto",
|
||||||
|
"carousel.current": "<sr>Sleid</sr> {current, number} / {max, number}",
|
||||||
|
"carousel.slide": "Sleid {current, number} o {max, number}",
|
||||||
"closed_registrations.other_server_instructions": "Gan fod Mastodon yn ddatganoledig, gallwch greu cyfrif ar weinydd arall a dal i ryngweithio gyda hwn.",
|
"closed_registrations.other_server_instructions": "Gan fod Mastodon yn ddatganoledig, gallwch greu cyfrif ar weinydd arall a dal i ryngweithio gyda hwn.",
|
||||||
"closed_registrations_modal.description": "Ar hyn o bryd nid yw'n bosib creu cyfrif ar {domain}, ond cadwch mewn cof nad oes raid i chi gael cyfrif yn benodol ar {domain} i ddefnyddio Mastodon.",
|
"closed_registrations_modal.description": "Ar hyn o bryd nid yw'n bosib creu cyfrif ar {domain}, ond cadwch mewn cof nad oes raid i chi gael cyfrif yn benodol ar {domain} i ddefnyddio Mastodon.",
|
||||||
"closed_registrations_modal.find_another_server": "Dod o hyd i weinydd arall",
|
"closed_registrations_modal.find_another_server": "Dod o hyd i weinydd arall",
|
||||||
@ -173,6 +175,8 @@
|
|||||||
"column.edit_list": "Golygu rhestr",
|
"column.edit_list": "Golygu rhestr",
|
||||||
"column.favourites": "Ffefrynnau",
|
"column.favourites": "Ffefrynnau",
|
||||||
"column.firehose": "Ffrydiau byw",
|
"column.firehose": "Ffrydiau byw",
|
||||||
|
"column.firehose_local": "Ffrwd fyw ar gyfer y gweinydd hwn",
|
||||||
|
"column.firehose_singular": "Ffrwd fyw",
|
||||||
"column.follow_requests": "Ceisiadau dilyn",
|
"column.follow_requests": "Ceisiadau dilyn",
|
||||||
"column.home": "Cartref",
|
"column.home": "Cartref",
|
||||||
"column.list_members": "Rheoli aelodau rhestr",
|
"column.list_members": "Rheoli aelodau rhestr",
|
||||||
@ -192,6 +196,7 @@
|
|||||||
"community.column_settings.local_only": "Lleol yn unig",
|
"community.column_settings.local_only": "Lleol yn unig",
|
||||||
"community.column_settings.media_only": "Cyfryngau yn unig",
|
"community.column_settings.media_only": "Cyfryngau yn unig",
|
||||||
"community.column_settings.remote_only": "Pell yn unig",
|
"community.column_settings.remote_only": "Pell yn unig",
|
||||||
|
"compose.error.blank_post": "Gall postiad ddim bod yn wag.",
|
||||||
"compose.language.change": "Newid iaith",
|
"compose.language.change": "Newid iaith",
|
||||||
"compose.language.search": "Chwilio ieithoedd...",
|
"compose.language.search": "Chwilio ieithoedd...",
|
||||||
"compose.published.body": "Postiad wedi ei gyhoeddi.",
|
"compose.published.body": "Postiad wedi ei gyhoeddi.",
|
||||||
@ -358,7 +363,9 @@
|
|||||||
"explore.trending_links": "Newyddion",
|
"explore.trending_links": "Newyddion",
|
||||||
"explore.trending_statuses": "Postiadau",
|
"explore.trending_statuses": "Postiadau",
|
||||||
"explore.trending_tags": "Hashnodau",
|
"explore.trending_tags": "Hashnodau",
|
||||||
|
"featured_carousel.current": "<sr>Postiad</sr> {current, number} / {max, number}",
|
||||||
"featured_carousel.header": "{count, plural, one {Postiad wedi'i binio} other {Postiadau wedi'u pinio}}",
|
"featured_carousel.header": "{count, plural, one {Postiad wedi'i binio} other {Postiadau wedi'u pinio}}",
|
||||||
|
"featured_carousel.slide": "Postiad {current, number} of {max, number}",
|
||||||
"filter_modal.added.context_mismatch_explanation": "Dyw'r categori hidlo hwn ddim yn berthnasol i'r cyd-destun yr ydych wedi cyrchu'r postiad hwn ynddo. Os ydych chi am i'r postiad gael ei hidlo yn y cyd-destun hwn hefyd, bydd yn rhaid i chi olygu'r hidlydd.",
|
"filter_modal.added.context_mismatch_explanation": "Dyw'r categori hidlo hwn ddim yn berthnasol i'r cyd-destun yr ydych wedi cyrchu'r postiad hwn ynddo. Os ydych chi am i'r postiad gael ei hidlo yn y cyd-destun hwn hefyd, bydd yn rhaid i chi olygu'r hidlydd.",
|
||||||
"filter_modal.added.context_mismatch_title": "Diffyg cyfatebiaeth cyd-destun!",
|
"filter_modal.added.context_mismatch_title": "Diffyg cyfatebiaeth cyd-destun!",
|
||||||
"filter_modal.added.expired_explanation": "Mae'r categori hidlydd hwn wedi dod i ben, bydd angen i chi newid y dyddiad dod i ben er mwyn iddo fod yn berthnasol.",
|
"filter_modal.added.expired_explanation": "Mae'r categori hidlydd hwn wedi dod i ben, bydd angen i chi newid y dyddiad dod i ben er mwyn iddo fod yn berthnasol.",
|
||||||
@ -401,6 +408,7 @@
|
|||||||
"follow_suggestions.who_to_follow": "Pwy i ddilyn",
|
"follow_suggestions.who_to_follow": "Pwy i ddilyn",
|
||||||
"followed_tags": "Hashnodau rydych yn eu dilyn",
|
"followed_tags": "Hashnodau rydych yn eu dilyn",
|
||||||
"footer.about": "Ynghylch",
|
"footer.about": "Ynghylch",
|
||||||
|
"footer.about_this_server": "Ynghylch",
|
||||||
"footer.directory": "Cyfeiriadur proffiliau",
|
"footer.directory": "Cyfeiriadur proffiliau",
|
||||||
"footer.get_app": "Llwytho'r ap i lawr",
|
"footer.get_app": "Llwytho'r ap i lawr",
|
||||||
"footer.keyboard_shortcuts": "Bysellau brys",
|
"footer.keyboard_shortcuts": "Bysellau brys",
|
||||||
@ -905,9 +913,12 @@
|
|||||||
"status.pin": "Pinio ar y proffil",
|
"status.pin": "Pinio ar y proffil",
|
||||||
"status.quote": "Dyfynnu",
|
"status.quote": "Dyfynnu",
|
||||||
"status.quote.cancel": "Diddymu'r dyfyniad",
|
"status.quote.cancel": "Diddymu'r dyfyniad",
|
||||||
|
"status.quote_error.blocked_account_hint.title": "Mae'r postiad hwn wedi'i guddio oherwydd eich bod wedi rhwystro @{name}.",
|
||||||
|
"status.quote_error.blocked_domain_hint.title": "Mae'r postiad hwn wedi'i guddio oherwydd eich bod wedi rhwystro {domain}.",
|
||||||
"status.quote_error.filtered": "Wedi'i guddio oherwydd un o'ch hidlwyr",
|
"status.quote_error.filtered": "Wedi'i guddio oherwydd un o'ch hidlwyr",
|
||||||
"status.quote_error.limited_account_hint.action": "Dangos beth bynnag",
|
"status.quote_error.limited_account_hint.action": "Dangos beth bynnag",
|
||||||
"status.quote_error.limited_account_hint.title": "Mae'r cyfrif hwn wedi'i guddio gan gymedrolwyr {domain}.",
|
"status.quote_error.limited_account_hint.title": "Mae'r cyfrif hwn wedi'i guddio gan gymedrolwyr {domain}.",
|
||||||
|
"status.quote_error.muted_account_hint.title": "Mae'r postiad hwn wedi'i guddio oherwydd eich bod wedi mudo @{name}.",
|
||||||
"status.quote_error.not_available": "Postiad ddim ar gael",
|
"status.quote_error.not_available": "Postiad ddim ar gael",
|
||||||
"status.quote_error.pending_approval": "Postiad yn yr arfaeth",
|
"status.quote_error.pending_approval": "Postiad yn yr arfaeth",
|
||||||
"status.quote_error.pending_approval_popout.body": "Ar Mastodon, gallwch reoli os yw rhywun yn gallu eich dyfynnu. Mae'r postiad hwn yn cael ei ddal nôl tra'n bod yn cael cymeradwyaeth yr awdur gwreiddiol.",
|
"status.quote_error.pending_approval_popout.body": "Ar Mastodon, gallwch reoli os yw rhywun yn gallu eich dyfynnu. Mae'r postiad hwn yn cael ei ddal nôl tra'n bod yn cael cymeradwyaeth yr awdur gwreiddiol.",
|
||||||
|
|||||||
@ -284,7 +284,7 @@
|
|||||||
"directory.recently_active": "Aktive for nyligt",
|
"directory.recently_active": "Aktive for nyligt",
|
||||||
"disabled_account_banner.account_settings": "Kontoindstillinger",
|
"disabled_account_banner.account_settings": "Kontoindstillinger",
|
||||||
"disabled_account_banner.text": "Din konto {disabledAccount} er pt. deaktiveret.",
|
"disabled_account_banner.text": "Din konto {disabledAccount} er pt. deaktiveret.",
|
||||||
"dismissable_banner.community_timeline": "Disse er de seneste offentlige indlæg fra personer med konti hostet af {domain}.",
|
"dismissable_banner.community_timeline": "Dette er de seneste offentlige indlæg fra personer med konti hostet af {domain}.",
|
||||||
"dismissable_banner.dismiss": "Afvis",
|
"dismissable_banner.dismiss": "Afvis",
|
||||||
"dismissable_banner.public_timeline": "Dette er de seneste offentlige indlæg fra personer på fediverset, som folk på {domain} følger.",
|
"dismissable_banner.public_timeline": "Dette er de seneste offentlige indlæg fra personer på fediverset, som folk på {domain} følger.",
|
||||||
"domain_block_modal.block": "Blokér server",
|
"domain_block_modal.block": "Blokér server",
|
||||||
|
|||||||
@ -249,6 +249,11 @@
|
|||||||
"confirmations.missing_alt_text.secondary": "Post anyway",
|
"confirmations.missing_alt_text.secondary": "Post anyway",
|
||||||
"confirmations.missing_alt_text.title": "Add alt text?",
|
"confirmations.missing_alt_text.title": "Add alt text?",
|
||||||
"confirmations.mute.confirm": "Mute",
|
"confirmations.mute.confirm": "Mute",
|
||||||
|
"confirmations.private_quote_notify.cancel": "Back to editing",
|
||||||
|
"confirmations.private_quote_notify.confirm": "Publish post",
|
||||||
|
"confirmations.private_quote_notify.do_not_show_again": "Don't show me this message again",
|
||||||
|
"confirmations.private_quote_notify.message": "The person you are quoting and other mentions will be notified and will be able to view your post, even if they're not following you.",
|
||||||
|
"confirmations.private_quote_notify.title": "Share with followers and mentioned users?",
|
||||||
"confirmations.quiet_post_quote_info.dismiss": "Don't remind me again",
|
"confirmations.quiet_post_quote_info.dismiss": "Don't remind me again",
|
||||||
"confirmations.quiet_post_quote_info.got_it": "Got it",
|
"confirmations.quiet_post_quote_info.got_it": "Got it",
|
||||||
"confirmations.quiet_post_quote_info.message": "When quoting a quiet public post, your post will be hidden from trending timelines.",
|
"confirmations.quiet_post_quote_info.message": "When quoting a quiet public post, your post will be hidden from trending timelines.",
|
||||||
@ -760,6 +765,7 @@
|
|||||||
"privacy_policy.title": "Privacy Policy",
|
"privacy_policy.title": "Privacy Policy",
|
||||||
"quote_error.edit": "Quotes cannot be added when editing a post.",
|
"quote_error.edit": "Quotes cannot be added when editing a post.",
|
||||||
"quote_error.poll": "Quoting is not allowed with polls.",
|
"quote_error.poll": "Quoting is not allowed with polls.",
|
||||||
|
"quote_error.private_mentions": "Quoting is not allowed with direct mentions.",
|
||||||
"quote_error.quote": "Only one quote at a time is allowed.",
|
"quote_error.quote": "Only one quote at a time is allowed.",
|
||||||
"quote_error.unauthorized": "You are not authorized to quote this post.",
|
"quote_error.unauthorized": "You are not authorized to quote this post.",
|
||||||
"quote_error.upload": "Quoting is not allowed with media attachments.",
|
"quote_error.upload": "Quoting is not allowed with media attachments.",
|
||||||
@ -1013,6 +1019,8 @@
|
|||||||
"video.volume_down": "Volume down",
|
"video.volume_down": "Volume down",
|
||||||
"video.volume_up": "Volume up",
|
"video.volume_up": "Volume up",
|
||||||
"visibility_modal.button_title": "Set visibility",
|
"visibility_modal.button_title": "Set visibility",
|
||||||
|
"visibility_modal.direct_quote_warning.text": "If you save the current settings, the embedded quote will be converted to a link.",
|
||||||
|
"visibility_modal.direct_quote_warning.title": "Quotes can't be embedded in private mentions",
|
||||||
"visibility_modal.header": "Visibility and interaction",
|
"visibility_modal.header": "Visibility and interaction",
|
||||||
"visibility_modal.helper.direct_quoting": "Private mentions authored on Mastodon can't be quoted by others.",
|
"visibility_modal.helper.direct_quoting": "Private mentions authored on Mastodon can't be quoted by others.",
|
||||||
"visibility_modal.helper.privacy_editing": "Visibility can't be changed after a post is published.",
|
"visibility_modal.helper.privacy_editing": "Visibility can't be changed after a post is published.",
|
||||||
|
|||||||
@ -743,7 +743,7 @@
|
|||||||
"poll.votes": "{votes, plural, one {# stem} other {# stemmen}}",
|
"poll.votes": "{votes, plural, one {# stem} other {# stemmen}}",
|
||||||
"poll_button.add_poll": "Peiling toevoegen",
|
"poll_button.add_poll": "Peiling toevoegen",
|
||||||
"poll_button.remove_poll": "Peiling verwijderen",
|
"poll_button.remove_poll": "Peiling verwijderen",
|
||||||
"privacy.change": "Privacy voor een bericht aanpassen",
|
"privacy.change": "Privacy van dit bericht aanpassen",
|
||||||
"privacy.direct.long": "Alleen voor mensen die specifiek in het bericht worden vermeld",
|
"privacy.direct.long": "Alleen voor mensen die specifiek in het bericht worden vermeld",
|
||||||
"privacy.direct.short": "Privébericht",
|
"privacy.direct.short": "Privébericht",
|
||||||
"privacy.private.long": "Alleen jouw volgers",
|
"privacy.private.long": "Alleen jouw volgers",
|
||||||
|
|||||||
@ -192,6 +192,7 @@
|
|||||||
"community.column_settings.local_only": "Vetëm vendore",
|
"community.column_settings.local_only": "Vetëm vendore",
|
||||||
"community.column_settings.media_only": "Vetëm Media",
|
"community.column_settings.media_only": "Vetëm Media",
|
||||||
"community.column_settings.remote_only": "Vetëm të largëta",
|
"community.column_settings.remote_only": "Vetëm të largëta",
|
||||||
|
"compose.error.blank_post": "Postimi s’mund të jetë i zbrazët.",
|
||||||
"compose.language.change": "Ndryshoni gjuhën",
|
"compose.language.change": "Ndryshoni gjuhën",
|
||||||
"compose.language.search": "Kërkoni te gjuhët…",
|
"compose.language.search": "Kërkoni te gjuhët…",
|
||||||
"compose.published.body": "Postimi u botua.",
|
"compose.published.body": "Postimi u botua.",
|
||||||
@ -403,6 +404,7 @@
|
|||||||
"follow_suggestions.who_to_follow": "Cilët të ndiqen",
|
"follow_suggestions.who_to_follow": "Cilët të ndiqen",
|
||||||
"followed_tags": "Hashtag-ë të ndjekur",
|
"followed_tags": "Hashtag-ë të ndjekur",
|
||||||
"footer.about": "Mbi",
|
"footer.about": "Mbi",
|
||||||
|
"footer.about_this_server": "Mbi",
|
||||||
"footer.directory": "Drejtori profilesh",
|
"footer.directory": "Drejtori profilesh",
|
||||||
"footer.get_app": "Merreni aplikacionin",
|
"footer.get_app": "Merreni aplikacionin",
|
||||||
"footer.keyboard_shortcuts": "Shkurtore tastiere",
|
"footer.keyboard_shortcuts": "Shkurtore tastiere",
|
||||||
|
|||||||
@ -606,8 +606,8 @@
|
|||||||
"notification.annual_report.view": "檢視 #Wrapstodon",
|
"notification.annual_report.view": "檢視 #Wrapstodon",
|
||||||
"notification.favourite": "{name} 已將您的嘟文加入最愛",
|
"notification.favourite": "{name} 已將您的嘟文加入最愛",
|
||||||
"notification.favourite.name_and_others_with_link": "{name} 與<a>{count, plural, other {其他 # 個人}}</a>已將您的嘟文加入最愛",
|
"notification.favourite.name_and_others_with_link": "{name} 與<a>{count, plural, other {其他 # 個人}}</a>已將您的嘟文加入最愛",
|
||||||
"notification.favourite_pm": "{name} 將您的私人提及加入最愛",
|
"notification.favourite_pm": "{name} 將您的私訊加入最愛",
|
||||||
"notification.favourite_pm.name_and_others_with_link": "{name} 與<a>{count, plural, other {其他 # 個人}}</a>已將您的私人提及加入最愛",
|
"notification.favourite_pm.name_and_others_with_link": "{name} 與<a>{count, plural, other {其他 # 個人}}</a>已將您的私訊加入最愛",
|
||||||
"notification.follow": "{name} 已跟隨您",
|
"notification.follow": "{name} 已跟隨您",
|
||||||
"notification.follow.name_and_others": "{name} 與<a>{count, plural, other {其他 # 個人}}</a>已跟隨您",
|
"notification.follow.name_and_others": "{name} 與<a>{count, plural, other {其他 # 個人}}</a>已跟隨您",
|
||||||
"notification.follow_request": "{name} 要求跟隨您",
|
"notification.follow_request": "{name} 要求跟隨您",
|
||||||
@ -1014,7 +1014,7 @@
|
|||||||
"video.volume_up": "提高音量",
|
"video.volume_up": "提高音量",
|
||||||
"visibility_modal.button_title": "設定可見性",
|
"visibility_modal.button_title": "設定可見性",
|
||||||
"visibility_modal.header": "可見性與互動",
|
"visibility_modal.header": "可見性與互動",
|
||||||
"visibility_modal.helper.direct_quoting": "Mastodon 上發佈之私人提及嘟文無法被其他使用者引用。",
|
"visibility_modal.helper.direct_quoting": "Mastodon 上發佈之私訊嘟文無法被其他使用者引用。",
|
||||||
"visibility_modal.helper.privacy_editing": "嘟文發布後無法變更可見性。",
|
"visibility_modal.helper.privacy_editing": "嘟文發布後無法變更可見性。",
|
||||||
"visibility_modal.helper.privacy_private_self_quote": "自我引用之私嘟無法設為公開可見。",
|
"visibility_modal.helper.privacy_private_self_quote": "自我引用之私嘟無法設為公開可見。",
|
||||||
"visibility_modal.helper.private_quoting": "Mastodon 上發佈之僅限跟隨者嘟文無法被其他使用者引用。",
|
"visibility_modal.helper.private_quoting": "Mastodon 上發佈之僅限跟隨者嘟文無法被其他使用者引用。",
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
changeComposeVisibility,
|
||||||
changeUploadCompose,
|
changeUploadCompose,
|
||||||
quoteCompose,
|
quoteCompose,
|
||||||
quoteComposeCancel,
|
quoteComposeCancel,
|
||||||
setComposeQuotePolicy,
|
setComposeQuotePolicy,
|
||||||
} from 'mastodon/actions/compose_typed';
|
pasteLinkCompose,
|
||||||
|
cancelPasteLinkCompose,
|
||||||
|
} from '@/mastodon/actions/compose_typed';
|
||||||
import { timelineDelete } from 'mastodon/actions/timelines_typed';
|
import { timelineDelete } from 'mastodon/actions/timelines_typed';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -38,7 +41,6 @@ import {
|
|||||||
COMPOSE_SENSITIVITY_CHANGE,
|
COMPOSE_SENSITIVITY_CHANGE,
|
||||||
COMPOSE_SPOILERNESS_CHANGE,
|
COMPOSE_SPOILERNESS_CHANGE,
|
||||||
COMPOSE_SPOILER_TEXT_CHANGE,
|
COMPOSE_SPOILER_TEXT_CHANGE,
|
||||||
COMPOSE_VISIBILITY_CHANGE,
|
|
||||||
COMPOSE_LANGUAGE_CHANGE,
|
COMPOSE_LANGUAGE_CHANGE,
|
||||||
COMPOSE_COMPOSING_CHANGE,
|
COMPOSE_COMPOSING_CHANGE,
|
||||||
COMPOSE_EMOJI_INSERT,
|
COMPOSE_EMOJI_INSERT,
|
||||||
@ -93,6 +95,7 @@ const initialState = ImmutableMap({
|
|||||||
quoted_status_id: null,
|
quoted_status_id: null,
|
||||||
quote_policy: 'public',
|
quote_policy: 'public',
|
||||||
default_quote_policy: 'public', // Set in hydration.
|
default_quote_policy: 'public', // Set in hydration.
|
||||||
|
fetching_link: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialPoll = ImmutableMap({
|
const initialPoll = ImmutableMap({
|
||||||
@ -315,7 +318,11 @@ const calculateProgress = (loaded, total) => Math.min(Math.round((loaded / total
|
|||||||
|
|
||||||
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
|
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
|
||||||
export const composeReducer = (state = initialState, action) => {
|
export const composeReducer = (state = initialState, action) => {
|
||||||
if (changeUploadCompose.fulfilled.match(action)) {
|
if (changeComposeVisibility.match(action)) {
|
||||||
|
return state
|
||||||
|
.set('privacy', action.payload)
|
||||||
|
.set('idempotencyKey', uuid());
|
||||||
|
} else if (changeUploadCompose.fulfilled.match(action)) {
|
||||||
return state
|
return state
|
||||||
.set('is_changing_upload', false)
|
.set('is_changing_upload', false)
|
||||||
.update('media_attachments', list => list.map(item => {
|
.update('media_attachments', list => list.map(item => {
|
||||||
@ -331,15 +338,27 @@ export const composeReducer = (state = initialState, action) => {
|
|||||||
return state.set('is_changing_upload', false);
|
return state.set('is_changing_upload', false);
|
||||||
} else if (quoteCompose.match(action)) {
|
} else if (quoteCompose.match(action)) {
|
||||||
const status = action.payload;
|
const status = action.payload;
|
||||||
|
const isDirect = state.get('privacy') === 'direct';
|
||||||
return state
|
return state
|
||||||
.set('quoted_status_id', status.get('id'))
|
.set('quoted_status_id', isDirect ? null : status.get('id'))
|
||||||
.set('spoiler', status.get('sensitive'))
|
.set('spoiler', status.get('sensitive'))
|
||||||
.set('spoiler_text', status.get('spoiler_text'))
|
.set('spoiler_text', status.get('spoiler_text'))
|
||||||
.update('privacy', (visibility) => ['public', 'unlisted'].includes(visibility) && status.get('visibility') === 'private' ? 'private' : visibility);
|
.update('privacy', (visibility) => {
|
||||||
|
if (['public', 'unlisted'].includes(visibility) && status.get('visibility') === 'private') {
|
||||||
|
return 'private';
|
||||||
|
}
|
||||||
|
return visibility;
|
||||||
|
});
|
||||||
} else if (quoteComposeCancel.match(action)) {
|
} else if (quoteComposeCancel.match(action)) {
|
||||||
return state.set('quoted_status_id', null);
|
return state.set('quoted_status_id', null);
|
||||||
} else if (setComposeQuotePolicy.match(action)) {
|
} else if (setComposeQuotePolicy.match(action)) {
|
||||||
return state.set('quote_policy', action.payload);
|
return state.set('quote_policy', action.payload);
|
||||||
|
} else if (pasteLinkCompose.pending.match(action)) {
|
||||||
|
return state.set('fetching_link', action.meta.requestId);
|
||||||
|
} else if (pasteLinkCompose.fulfilled.match(action) || pasteLinkCompose.rejected.match(action)) {
|
||||||
|
return action.meta.requestId === state.get('fetching_link') ? state.set('fetching_link', null) : state;
|
||||||
|
} else if (cancelPasteLinkCompose.match(action)) {
|
||||||
|
return state.set('fetching_link', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(action.type) {
|
switch(action.type) {
|
||||||
@ -383,10 +402,6 @@ export const composeReducer = (state = initialState, action) => {
|
|||||||
return state
|
return state
|
||||||
.set('spoiler_text', action.text)
|
.set('spoiler_text', action.text)
|
||||||
.set('idempotencyKey', uuid());
|
.set('idempotencyKey', uuid());
|
||||||
case COMPOSE_VISIBILITY_CHANGE:
|
|
||||||
return state
|
|
||||||
.set('privacy', action.value)
|
|
||||||
.set('idempotencyKey', uuid());
|
|
||||||
case COMPOSE_CHANGE:
|
case COMPOSE_CHANGE:
|
||||||
return state
|
return state
|
||||||
.set('text', action.text)
|
.set('text', action.text)
|
||||||
|
|||||||
@ -42,7 +42,7 @@ interface AppThunkConfig {
|
|||||||
}
|
}
|
||||||
export type AppThunkApi = Pick<
|
export type AppThunkApi = Pick<
|
||||||
GetThunkAPI<AppThunkConfig>,
|
GetThunkAPI<AppThunkConfig>,
|
||||||
'getState' | 'dispatch'
|
'getState' | 'dispatch' | 'requestId'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
interface AppThunkOptions<Arg> {
|
interface AppThunkOptions<Arg> {
|
||||||
@ -60,7 +60,7 @@ type AppThunk<Arg = void, Returned = void> = (
|
|||||||
|
|
||||||
type AppThunkCreator<Arg = void, Returned = void, ExtraArg = unknown> = (
|
type AppThunkCreator<Arg = void, Returned = void, ExtraArg = unknown> = (
|
||||||
arg: Arg,
|
arg: Arg,
|
||||||
api: AppThunkApi,
|
api: Pick<AppThunkApi, 'getState' | 'dispatch'>,
|
||||||
extra?: ExtraArg,
|
extra?: ExtraArg,
|
||||||
) => Returned;
|
) => Returned;
|
||||||
|
|
||||||
@ -143,10 +143,10 @@ export function createAsyncThunk<Arg = void, Returned = void>(
|
|||||||
name,
|
name,
|
||||||
async (
|
async (
|
||||||
arg: Arg,
|
arg: Arg,
|
||||||
{ getState, dispatch, fulfillWithValue, rejectWithValue },
|
{ getState, dispatch, requestId, fulfillWithValue, rejectWithValue },
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const result = await creator(arg, { dispatch, getState });
|
const result = await creator(arg, { dispatch, getState, requestId });
|
||||||
|
|
||||||
return fulfillWithValue(result, {
|
return fulfillWithValue(result, {
|
||||||
useLoadingBar: options.useLoadingBar,
|
useLoadingBar: options.useLoadingBar,
|
||||||
@ -280,10 +280,11 @@ export function createDataLoadingThunk<
|
|||||||
|
|
||||||
return createAsyncThunk<Args, Returned>(
|
return createAsyncThunk<Args, Returned>(
|
||||||
name,
|
name,
|
||||||
async (arg, { getState, dispatch }) => {
|
async (arg, { getState, dispatch, requestId }) => {
|
||||||
const data = await loadData(arg, {
|
const data = await loadData(arg, {
|
||||||
dispatch,
|
dispatch,
|
||||||
getState,
|
getState,
|
||||||
|
requestId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!onData) return data as Returned;
|
if (!onData) return data as Returned;
|
||||||
@ -291,6 +292,7 @@ export function createDataLoadingThunk<
|
|||||||
const result = await onData(data, {
|
const result = await onData(data, {
|
||||||
dispatch,
|
dispatch,
|
||||||
getState,
|
getState,
|
||||||
|
requestId,
|
||||||
discardLoadData: discardLoadDataInPayload,
|
discardLoadData: discardLoadDataInPayload,
|
||||||
actionArg: arg,
|
actionArg: arg,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1325,6 +1325,10 @@ a.sparkline {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
animation: skeleton 1.2s ease-in-out infinite;
|
animation: skeleton 1.2s ease-in-out infinite;
|
||||||
|
|
||||||
|
.reduce-motion & {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes skeleton {
|
@keyframes skeleton {
|
||||||
|
|||||||
@ -2685,6 +2685,7 @@ a.account__display-name {
|
|||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--zoomed-in {
|
&--zoomed-in {
|
||||||
@ -5743,6 +5744,34 @@ a.status-card {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.visibility-modal {
|
||||||
|
&__quote-warning {
|
||||||
|
color: var(--nested-card-text);
|
||||||
|
background:
|
||||||
|
/* This is a bit of a silly hack for layering two background colours
|
||||||
|
* since --nested-card-background is too transparent for a tooltip */
|
||||||
|
linear-gradient(
|
||||||
|
var(--nested-card-background),
|
||||||
|
var(--nested-card-background)
|
||||||
|
),
|
||||||
|
linear-gradient(var(--background-color), var(--background-color));
|
||||||
|
border: var(--nested-card-border);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: $darker-text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: $dark-text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.visibility-dropdown {
|
.visibility-dropdown {
|
||||||
&__overlay[data-popper-placement] {
|
&__overlay[data-popper-placement] {
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
@ -6096,6 +6125,7 @@ a.status-card {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
touch-action: pan-y;
|
||||||
|
|
||||||
&__buttons {
|
&__buttons {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -6131,11 +6161,17 @@ a.status-card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.media-modal__closer {
|
.media-modal__closer {
|
||||||
|
display: flex;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-modal__navigation {
|
.media-modal__navigation {
|
||||||
@ -8936,6 +8972,10 @@ noscript {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
.layout-multiple-columns & {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: (124px + 300px)) {
|
@media screen and (max-width: (124px + 300px)) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SignedRequest
|
class SignedRequest
|
||||||
include DomainControlHelper
|
|
||||||
|
|
||||||
EXPIRATION_WINDOW_LIMIT = 12.hours
|
EXPIRATION_WINDOW_LIMIT = 12.hours
|
||||||
CLOCK_SKEW_MARGIN = 1.hour
|
CLOCK_SKEW_MARGIN = 1.hour
|
||||||
|
|
||||||
|
|||||||
@ -102,6 +102,7 @@ class Form::AdminSettings
|
|||||||
DOMAIN_BLOCK_AUDIENCES = %w(disabled users all).freeze
|
DOMAIN_BLOCK_AUDIENCES = %w(disabled users all).freeze
|
||||||
REGISTRATION_MODES = %w(open approved none).freeze
|
REGISTRATION_MODES = %w(open approved none).freeze
|
||||||
FEED_ACCESS_MODES = %w(public authenticated disabled).freeze
|
FEED_ACCESS_MODES = %w(public authenticated disabled).freeze
|
||||||
|
ALTERNATE_FEED_ACCESS_MODES = %w(public authenticated).freeze
|
||||||
LANDING_PAGE = %w(trends about local_feed).freeze
|
LANDING_PAGE = %w(trends about local_feed).freeze
|
||||||
|
|
||||||
attr_accessor(*KEYS)
|
attr_accessor(*KEYS)
|
||||||
@ -114,7 +115,7 @@ class Form::AdminSettings
|
|||||||
validates :show_domain_blocks_rationale, inclusion: { in: DOMAIN_BLOCK_AUDIENCES }, if: -> { defined?(@show_domain_blocks_rationale) }
|
validates :show_domain_blocks_rationale, inclusion: { in: DOMAIN_BLOCK_AUDIENCES }, if: -> { defined?(@show_domain_blocks_rationale) }
|
||||||
validates :local_live_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@local_live_feed_access) }
|
validates :local_live_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@local_live_feed_access) }
|
||||||
validates :remote_live_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@remote_live_feed_access) }
|
validates :remote_live_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@remote_live_feed_access) }
|
||||||
validates :local_topic_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@local_topic_feed_access) }
|
validates :local_topic_feed_access, inclusion: { in: ALTERNATE_FEED_ACCESS_MODES }, if: -> { defined?(@local_topic_feed_access) }
|
||||||
validates :remote_topic_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@remote_topic_feed_access) }
|
validates :remote_topic_feed_access, inclusion: { in: FEED_ACCESS_MODES }, if: -> { defined?(@remote_topic_feed_access) }
|
||||||
validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) }
|
validates :media_cache_retention_period, :content_cache_retention_period, :backups_retention_period, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@media_cache_retention_period) || defined?(@content_cache_retention_period) || defined?(@backups_retention_period) }
|
||||||
validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) }
|
validates :min_age, numericality: { only_integer: true }, allow_blank: true, if: -> { defined?(@min_age) }
|
||||||
|
|||||||
@ -46,7 +46,7 @@
|
|||||||
.fields-row
|
.fields-row
|
||||||
.fields-row__column.fields-row__column-6.fields-group
|
.fields-row__column.fields-row__column-6.fields-group
|
||||||
= f.input :local_topic_feed_access,
|
= f.input :local_topic_feed_access,
|
||||||
collection: f.object.class::FEED_ACCESS_MODES,
|
collection: f.object.class::ALTERNATE_FEED_ACCESS_MODES,
|
||||||
include_blank: false,
|
include_blank: false,
|
||||||
label_method: ->(mode) { I18n.t("admin.settings.feed_access.modes.#{mode}") },
|
label_method: ->(mode) { I18n.t("admin.settings.feed_access.modes.#{mode}") },
|
||||||
wrapper: :with_label
|
wrapper: :with_label
|
||||||
|
|||||||
@ -5,7 +5,7 @@ class ActivityPub::RefetchAndVerifyQuoteWorker
|
|||||||
include ExponentialBackoff
|
include ExponentialBackoff
|
||||||
include JsonLdHelper
|
include JsonLdHelper
|
||||||
|
|
||||||
sidekiq_options queue: 'pull', retry: 3
|
sidekiq_options queue: 'pull', retry: 5
|
||||||
|
|
||||||
def perform(quote_id, quoted_uri, options = {})
|
def perform(quote_id, quoted_uri, options = {})
|
||||||
quote = Quote.find(quote_id)
|
quote = Quote.find(quote_id)
|
||||||
|
|||||||
@ -2015,6 +2015,7 @@ be:
|
|||||||
errors:
|
errors:
|
||||||
in_reply_not_found: Здаецца, допіс, на які вы спрабуеце адказаць, не існуе.
|
in_reply_not_found: Здаецца, допіс, на які вы спрабуеце адказаць, не існуе.
|
||||||
quoted_status_not_found: Выглядае, што допісу, які Вы спрабуеце цытаваць, не існуе.
|
quoted_status_not_found: Выглядае, што допісу, які Вы спрабуеце цытаваць, не існуе.
|
||||||
|
quoted_user_not_mentioned: Немагчыма цытаваць незгаданага карыстальніка ў допісе прыватнага згадвання.
|
||||||
over_character_limit: перавышаная колькасць сімвалаў у %{max}
|
over_character_limit: перавышаная колькасць сімвалаў у %{max}
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: Допісы, бачныя толькі згаданым карыстальнікам, нельга замацаваць
|
direct: Допісы, бачныя толькі згаданым карыстальнікам, нельга замацаваць
|
||||||
|
|||||||
@ -911,6 +911,11 @@ cy:
|
|||||||
authenticated: Defnyddwyr dilys yn unig
|
authenticated: Defnyddwyr dilys yn unig
|
||||||
disabled: Gofyn am rôl defnyddiwr penodol
|
disabled: Gofyn am rôl defnyddiwr penodol
|
||||||
public: Pawb
|
public: Pawb
|
||||||
|
landing_page:
|
||||||
|
values:
|
||||||
|
about: Ynghylch
|
||||||
|
local_feed: Ffrwd leol
|
||||||
|
trends: Trendiau
|
||||||
registrations:
|
registrations:
|
||||||
moderation_recommandation: Gwnewch yn siŵr bod gennych chi dîm cymedroli digonol ac adweithiol cyn i chi agor cofrestriadau i bawb!
|
moderation_recommandation: Gwnewch yn siŵr bod gennych chi dîm cymedroli digonol ac adweithiol cyn i chi agor cofrestriadau i bawb!
|
||||||
preamble: Rheoli pwy all greu cyfrif ar eich gweinydd.
|
preamble: Rheoli pwy all greu cyfrif ar eich gweinydd.
|
||||||
|
|||||||
@ -1929,6 +1929,7 @@ da:
|
|||||||
errors:
|
errors:
|
||||||
in_reply_not_found: Indlægget, der forsøges besvaret, ser ikke ud til at eksistere.
|
in_reply_not_found: Indlægget, der forsøges besvaret, ser ikke ud til at eksistere.
|
||||||
quoted_status_not_found: Indlægget, du forsøger at citere, ser ikke ud til at eksistere.
|
quoted_status_not_found: Indlægget, du forsøger at citere, ser ikke ud til at eksistere.
|
||||||
|
quoted_user_not_mentioned: Kan ikke citere en ikke-omtalt bruger i et privat omtale-indlæg.
|
||||||
over_character_limit: grænsen på %{max} tegn overskredet
|
over_character_limit: grænsen på %{max} tegn overskredet
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: Indlæg, som kun kan ses af omtalte brugere, kan ikke fastgøres
|
direct: Indlæg, som kun kan ses af omtalte brugere, kan ikke fastgøres
|
||||||
|
|||||||
@ -1929,6 +1929,7 @@ de:
|
|||||||
errors:
|
errors:
|
||||||
in_reply_not_found: Der Beitrag, auf den du antworten möchtest, scheint nicht zu existieren.
|
in_reply_not_found: Der Beitrag, auf den du antworten möchtest, scheint nicht zu existieren.
|
||||||
quoted_status_not_found: Der Beitrag, den du zitieren möchtest, scheint nicht zu existieren.
|
quoted_status_not_found: Der Beitrag, den du zitieren möchtest, scheint nicht zu existieren.
|
||||||
|
quoted_user_not_mentioned: Ein nicht erwähnter Nutzer kann in einem privaten Erwähnungsbeitrag nicht zitiert werden.
|
||||||
over_character_limit: Begrenzung von %{max} Zeichen überschritten
|
over_character_limit: Begrenzung von %{max} Zeichen überschritten
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: Beiträge, die nur für erwähnte Profile sichtbar sind, können nicht angeheftet werden
|
direct: Beiträge, die nur für erwähnte Profile sichtbar sind, können nicht angeheftet werden
|
||||||
|
|||||||
@ -1929,6 +1929,7 @@ el:
|
|||||||
errors:
|
errors:
|
||||||
in_reply_not_found: Η ανάρτηση στην οποία προσπαθείς να απαντήσεις δεν φαίνεται να υπάρχει.
|
in_reply_not_found: Η ανάρτηση στην οποία προσπαθείς να απαντήσεις δεν φαίνεται να υπάρχει.
|
||||||
quoted_status_not_found: Η ανάρτηση την οποία προσπαθείς να παραθέσεις δεν φαίνεται να υπάρχει.
|
quoted_status_not_found: Η ανάρτηση την οποία προσπαθείς να παραθέσεις δεν φαίνεται να υπάρχει.
|
||||||
|
quoted_user_not_mentioned: Δεν είναι δυνατή η παράθεση ενός μη επισημασμένου χρήστη σε μια ανάρτηση Ιδιωτικής επισήμανσης.
|
||||||
over_character_limit: υπέρβαση μέγιστου ορίου %{max} χαρακτήρων
|
over_character_limit: υπέρβαση μέγιστου ορίου %{max} χαρακτήρων
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: Αναρτήσεις που είναι ορατές μόνο στους αναφερόμενους χρήστες δεν μπορούν να καρφιτσωθούν
|
direct: Αναρτήσεις που είναι ορατές μόνο στους αναφερόμενους χρήστες δεν μπορούν να καρφιτσωθούν
|
||||||
|
|||||||
@ -1929,6 +1929,7 @@ es-AR:
|
|||||||
errors:
|
errors:
|
||||||
in_reply_not_found: El mensaje al que intentás responder no existe.
|
in_reply_not_found: El mensaje al que intentás responder no existe.
|
||||||
quoted_status_not_found: El mensaje al que intentás citar parece que no existe.
|
quoted_status_not_found: El mensaje al que intentás citar parece que no existe.
|
||||||
|
quoted_user_not_mentioned: No se puede citar a un usuario no mencionado en un mensaje de mención privada.
|
||||||
over_character_limit: se excedió el límite de %{max} caracteres
|
over_character_limit: se excedió el límite de %{max} caracteres
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: Los mensajes que sólo son visibles para los usuarios mencionados no pueden ser fijados
|
direct: Los mensajes que sólo son visibles para los usuarios mencionados no pueden ser fijados
|
||||||
|
|||||||
@ -1929,6 +1929,7 @@ fi:
|
|||||||
errors:
|
errors:
|
||||||
in_reply_not_found: Julkaisua, johon yrität vastata, ei näytä olevan olemassa.
|
in_reply_not_found: Julkaisua, johon yrität vastata, ei näytä olevan olemassa.
|
||||||
quoted_status_not_found: Julkaisua, jota yrität lainata, ei näytä olevan olemassa.
|
quoted_status_not_found: Julkaisua, jota yrität lainata, ei näytä olevan olemassa.
|
||||||
|
quoted_user_not_mentioned: Mainitsematonta käyttäjää ei voi lainata yksityismaininnassa.
|
||||||
over_character_limit: merkkimäärän rajoitus %{max} ylitetty
|
over_character_limit: merkkimäärän rajoitus %{max} ylitetty
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: Vain mainituille käyttäjille näkyviä julkaisuja ei voi kiinnittää
|
direct: Vain mainituille käyttäjille näkyviä julkaisuja ei voi kiinnittää
|
||||||
|
|||||||
@ -2015,6 +2015,7 @@ he:
|
|||||||
errors:
|
errors:
|
||||||
in_reply_not_found: נראה שההודעה שנסית להגיב לה לא קיימת.
|
in_reply_not_found: נראה שההודעה שנסית להגיב לה לא קיימת.
|
||||||
quoted_status_not_found: נראה שההודעה שנסית לצטט לא קיימת.
|
quoted_status_not_found: נראה שההודעה שנסית לצטט לא קיימת.
|
||||||
|
quoted_user_not_mentioned: לא ניתן לצטט משתמש שאיננו מאוזכר בהודעה פרטית.
|
||||||
over_character_limit: חריגה מגבול התווים של %{max}
|
over_character_limit: חריגה מגבול התווים של %{max}
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: לא ניתן לקבע הודעות שנראותן מוגבלת למכותבים בלבד
|
direct: לא ניתן לקבע הודעות שנראותן מוגבלת למכותבים בלבד
|
||||||
|
|||||||
@ -1933,6 +1933,7 @@ is:
|
|||||||
errors:
|
errors:
|
||||||
in_reply_not_found: Færslan sem þú ert að reyna að svara að er líklega ekki til.
|
in_reply_not_found: Færslan sem þú ert að reyna að svara að er líklega ekki til.
|
||||||
quoted_status_not_found: Færslan sem þú ert að reyna að vitna í virðist ekki vera til.
|
quoted_status_not_found: Færslan sem þú ert að reyna að vitna í virðist ekki vera til.
|
||||||
|
quoted_user_not_mentioned: Ekki er hægt að vitna í aðila sem ekki er minnst á í einkaspjalli.
|
||||||
over_character_limit: hámarksfjölda stafa (%{max}) náð
|
over_character_limit: hámarksfjölda stafa (%{max}) náð
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: Ekki er hægt að festa færslur sem einungis eru sýnilegar þeim notendum sem minnst er á
|
direct: Ekki er hægt að festa færslur sem einungis eru sýnilegar þeim notendum sem minnst er á
|
||||||
|
|||||||
@ -1929,6 +1929,7 @@ nl:
|
|||||||
errors:
|
errors:
|
||||||
in_reply_not_found: Het bericht waarop je probeert te reageren lijkt niet te bestaan.
|
in_reply_not_found: Het bericht waarop je probeert te reageren lijkt niet te bestaan.
|
||||||
quoted_status_not_found: Het bericht die je probeert te citeren lijkt niet te bestaan.
|
quoted_status_not_found: Het bericht die je probeert te citeren lijkt niet te bestaan.
|
||||||
|
quoted_user_not_mentioned: Een niet-vermelde gebruiker kan niet in een privébericht worden geciteerd.
|
||||||
over_character_limit: Limiet van %{max} tekens overschreden
|
over_character_limit: Limiet van %{max} tekens overschreden
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: Berichten die alleen zichtbaar zijn voor vermelde gebruikers, kunnen niet worden vastgezet
|
direct: Berichten die alleen zichtbaar zijn voor vermelde gebruikers, kunnen niet worden vastgezet
|
||||||
|
|||||||
@ -93,6 +93,7 @@ cy:
|
|||||||
content_cache_retention_period: Bydd yr holl bostiadau gan weinyddion eraill (gan gynnwys hwb ac atebion) yn cael eu dileu ar ôl y nifer penodedig o ddyddiau, heb ystyried unrhyw ryngweithio defnyddiwr lleol â'r postiadau hynny. Mae hyn yn cynnwys postiadau lle mae defnyddiwr lleol wedi ei farcio fel nodau tudalen neu ffefrynnau. Bydd cyfeiriadau preifat rhwng defnyddwyr o wahanol achosion hefyd yn cael eu colli ac yn amhosibl eu hadfer. Mae'r defnydd o'r gosodiad hwn wedi'i fwriadu ar gyfer achosion pwrpas arbennig ac mae'n torri llawer o ddisgwyliadau defnyddwyr pan gaiff ei weithredu at ddibenion cyffredinol.
|
content_cache_retention_period: Bydd yr holl bostiadau gan weinyddion eraill (gan gynnwys hwb ac atebion) yn cael eu dileu ar ôl y nifer penodedig o ddyddiau, heb ystyried unrhyw ryngweithio defnyddiwr lleol â'r postiadau hynny. Mae hyn yn cynnwys postiadau lle mae defnyddiwr lleol wedi ei farcio fel nodau tudalen neu ffefrynnau. Bydd cyfeiriadau preifat rhwng defnyddwyr o wahanol achosion hefyd yn cael eu colli ac yn amhosibl eu hadfer. Mae'r defnydd o'r gosodiad hwn wedi'i fwriadu ar gyfer achosion pwrpas arbennig ac mae'n torri llawer o ddisgwyliadau defnyddwyr pan gaiff ei weithredu at ddibenion cyffredinol.
|
||||||
custom_css: Gallwch gymhwyso arddulliau cyfaddas ar fersiwn gwe Mastodon.
|
custom_css: Gallwch gymhwyso arddulliau cyfaddas ar fersiwn gwe Mastodon.
|
||||||
favicon: WEBP, PNG, GIF neu JPG. Yn diystyru'r favicon Mastodon rhagosodedig gydag eicon cyfaddas.
|
favicon: WEBP, PNG, GIF neu JPG. Yn diystyru'r favicon Mastodon rhagosodedig gydag eicon cyfaddas.
|
||||||
|
landing_page: Yn dewis pa dudalen y mae ymwelwyr newydd yn ei gweld pan fyddan nhw'n cyrraedd eich gweinydd am y tro cyntaf. Os dewiswch "Trendio", yna mae angen galluogi tueddiadau yn y Gosodiadau Darganfod. Os dewiswch "Ffrydiau lleol", yna mae angen gosod "Mynediad i ffrydiau byw sy'n cynnwys postiadau lleol" i "Pawb" yn y Gosodiadau Darganfod.
|
||||||
mascot: Yn diystyru'r darlun yn y rhyngwyneb gwe uwch.
|
mascot: Yn diystyru'r darlun yn y rhyngwyneb gwe uwch.
|
||||||
media_cache_retention_period: Mae ffeiliau cyfryngau o bostiadau a wneir gan ddefnyddwyr o bell yn cael eu storio ar eich gweinydd. Pan gaiff ei osod i werth positif, bydd y cyfryngau yn cael eu dileu ar ôl y nifer penodedig o ddyddiau. Os gofynnir am y data cyfryngau ar ôl iddo gael ei ddileu, caiff ei ail-lwytho i lawr, os yw'r cynnwys ffynhonnell yn dal i fod ar gael. Oherwydd cyfyngiadau ar ba mor aml y mae cardiau rhagolwg cyswllt yn pleidleisio i wefannau trydydd parti, argymhellir gosod y gwerth hwn i o leiaf 14 diwrnod, neu ni fydd cardiau rhagolwg cyswllt yn cael eu diweddaru ar alw cyn yr amser hwnnw.
|
media_cache_retention_period: Mae ffeiliau cyfryngau o bostiadau a wneir gan ddefnyddwyr o bell yn cael eu storio ar eich gweinydd. Pan gaiff ei osod i werth positif, bydd y cyfryngau yn cael eu dileu ar ôl y nifer penodedig o ddyddiau. Os gofynnir am y data cyfryngau ar ôl iddo gael ei ddileu, caiff ei ail-lwytho i lawr, os yw'r cynnwys ffynhonnell yn dal i fod ar gael. Oherwydd cyfyngiadau ar ba mor aml y mae cardiau rhagolwg cyswllt yn pleidleisio i wefannau trydydd parti, argymhellir gosod y gwerth hwn i o leiaf 14 diwrnod, neu ni fydd cardiau rhagolwg cyswllt yn cael eu diweddaru ar alw cyn yr amser hwnnw.
|
||||||
min_age: Mae gofyn i ddefnyddwyr gadarnhau eu dyddiad geni wrth gofrestru
|
min_age: Mae gofyn i ddefnyddwyr gadarnhau eu dyddiad geni wrth gofrestru
|
||||||
@ -290,6 +291,7 @@ cy:
|
|||||||
content_cache_retention_period: Cyfnod cadw cynnwys o bell
|
content_cache_retention_period: Cyfnod cadw cynnwys o bell
|
||||||
custom_css: CSS cyfaddas
|
custom_css: CSS cyfaddas
|
||||||
favicon: Favicon
|
favicon: Favicon
|
||||||
|
landing_page: Tudalen cychwyn ar gyfer ymwelwyr newydd
|
||||||
local_live_feed_access: Mynediad i ffrydiau byw sy'n cynnwys postiadau lleol
|
local_live_feed_access: Mynediad i ffrydiau byw sy'n cynnwys postiadau lleol
|
||||||
local_topic_feed_access: Mynediad i ffrydiau hashnod a dolenni sy'n cynnwys postiadau lleol
|
local_topic_feed_access: Mynediad i ffrydiau hashnod a dolenni sy'n cynnwys postiadau lleol
|
||||||
mascot: Mascot cyfaddas (hen)
|
mascot: Mascot cyfaddas (hen)
|
||||||
|
|||||||
@ -79,7 +79,7 @@ en:
|
|||||||
featured_tag:
|
featured_tag:
|
||||||
name: 'Here are some of the hashtags you used the most recently:'
|
name: 'Here are some of the hashtags you used the most recently:'
|
||||||
filters:
|
filters:
|
||||||
action: Chose which action to perform when a post matches the filter
|
action: Choose which action to perform when a post matches the filter
|
||||||
actions:
|
actions:
|
||||||
blur: Hide media behind a warning, without hiding the text itself
|
blur: Hide media behind a warning, without hiding the text itself
|
||||||
hide: Completely hide the filtered content, behaving as if it did not exist
|
hide: Completely hide the filtered content, behaving as if it did not exist
|
||||||
|
|||||||
@ -1914,6 +1914,7 @@ sq:
|
|||||||
errors:
|
errors:
|
||||||
in_reply_not_found: Gjendja të cilës po provoni t’i përgjigjeni s’duket se ekziston.
|
in_reply_not_found: Gjendja të cilës po provoni t’i përgjigjeni s’duket se ekziston.
|
||||||
quoted_status_not_found: Postimi që po rrekeni të citoni nuk duket se ekziston.
|
quoted_status_not_found: Postimi që po rrekeni të citoni nuk duket se ekziston.
|
||||||
|
quoted_user_not_mentioned: S’mund të citohet një përdorues që s’është përmendur në një postim Përmendje Private.
|
||||||
over_character_limit: u tejkalua kufi shenjash prej %{max}
|
over_character_limit: u tejkalua kufi shenjash prej %{max}
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: Postimet që janë të dukshme vetëm për përdoruesit e përmendur s’mund të fiksohen
|
direct: Postimet që janë të dukshme vetëm për përdoruesit e përmendur s’mund të fiksohen
|
||||||
|
|||||||
@ -1886,6 +1886,7 @@ vi:
|
|||||||
errors:
|
errors:
|
||||||
in_reply_not_found: Bạn đang trả lời một tút không còn tồn tại.
|
in_reply_not_found: Bạn đang trả lời một tút không còn tồn tại.
|
||||||
quoted_status_not_found: Bạn đang trích dẫn một tút không còn tồn tại.
|
quoted_status_not_found: Bạn đang trích dẫn một tút không còn tồn tại.
|
||||||
|
quoted_user_not_mentioned: Không thể trích dẫn người dùng không được nhắc đến trong tút Nhắn riêng.
|
||||||
over_character_limit: vượt quá giới hạn %{max} ký tự
|
over_character_limit: vượt quá giới hạn %{max} ký tự
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: Không thể ghim những tút nhắn riêng
|
direct: Không thể ghim những tút nhắn riêng
|
||||||
|
|||||||
@ -1888,6 +1888,7 @@ zh-TW:
|
|||||||
errors:
|
errors:
|
||||||
in_reply_not_found: 您嘗試回覆之嘟文似乎不存在。
|
in_reply_not_found: 您嘗試回覆之嘟文似乎不存在。
|
||||||
quoted_status_not_found: 您嘗試引用之嘟文似乎不存在。
|
quoted_status_not_found: 您嘗試引用之嘟文似乎不存在。
|
||||||
|
quoted_user_not_mentioned: 無法於私訊嘟文中引用無提及之使用者。
|
||||||
over_character_limit: 已超過 %{max} 字的限制
|
over_character_limit: 已超過 %{max} 字的限制
|
||||||
pin_errors:
|
pin_errors:
|
||||||
direct: 無法釘選只有僅提及使用者可見之嘟文
|
direct: 無法釘選只有僅提及使用者可見之嘟文
|
||||||
|
|||||||
27
package.json
27
package.json
@ -48,7 +48,7 @@
|
|||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
"@github/webauthn-json": "^2.1.1",
|
"@github/webauthn-json": "^2.1.1",
|
||||||
"@optimize-lodash/rollup-plugin": "^5.0.2",
|
"@optimize-lodash/rollup-plugin": "^5.0.2",
|
||||||
"@rails/ujs": "7.1.502",
|
"@rails/ujs": "7.1.600",
|
||||||
"@react-spring/web": "^9.7.5",
|
"@react-spring/web": "^9.7.5",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"@use-gesture/react": "^10.3.1",
|
"@use-gesture/react": "^10.3.1",
|
||||||
@ -103,7 +103,6 @@
|
|||||||
"react-router-dom": "^5.3.4",
|
"react-router-dom": "^5.3.4",
|
||||||
"react-select": "^5.7.3",
|
"react-select": "^5.7.3",
|
||||||
"react-sparklines": "^1.7.0",
|
"react-sparklines": "^1.7.0",
|
||||||
"react-swipeable-views": "^0.14.0",
|
|
||||||
"react-textarea-autosize": "^8.4.1",
|
"react-textarea-autosize": "^8.4.1",
|
||||||
"react-toggle": "^4.1.3",
|
"react-toggle": "^4.1.3",
|
||||||
"redux-immutable": "^4.0.0",
|
"redux-immutable": "^4.0.0",
|
||||||
@ -135,10 +134,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.23.0",
|
"@eslint/js": "^9.23.0",
|
||||||
"@formatjs/cli": "^6.1.1",
|
"@formatjs/cli": "^6.1.1",
|
||||||
"@storybook/addon-a11y": "^9.1.1",
|
"@storybook/addon-a11y": "^10.0.2",
|
||||||
"@storybook/addon-docs": "^9.1.1",
|
"@storybook/addon-docs": "^10.0.2",
|
||||||
"@storybook/addon-vitest": "^9.1.1",
|
"@storybook/addon-vitest": "^10.0.2",
|
||||||
"@storybook/react-vite": "^9.1.1",
|
"@storybook/react-vite": "^10.0.2",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@types/debug": "^4",
|
"@types/debug": "^4",
|
||||||
@ -160,14 +159,14 @@
|
|||||||
"@types/react-router": "^5.1.20",
|
"@types/react-router": "^5.1.20",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-sparklines": "^1.7.2",
|
"@types/react-sparklines": "^1.7.2",
|
||||||
"@types/react-swipeable-views": "^0.13.1",
|
|
||||||
"@types/react-test-renderer": "^18.0.0",
|
"@types/react-test-renderer": "^18.0.0",
|
||||||
"@types/react-toggle": "^4.0.3",
|
"@types/react-toggle": "^4.0.3",
|
||||||
"@types/redux-immutable": "^4.0.3",
|
"@types/redux-immutable": "^4.0.3",
|
||||||
"@types/requestidlecallback": "^0.3.5",
|
"@types/requestidlecallback": "^0.3.5",
|
||||||
"@vitest/browser": "^3.2.4",
|
"@vitest/browser": "^4.0.5",
|
||||||
"@vitest/coverage-v8": "^3.2.4",
|
"@vitest/browser-playwright": "^4.0.5",
|
||||||
"@vitest/ui": "^3.2.4",
|
"@vitest/coverage-v8": "^4.0.5",
|
||||||
|
"@vitest/ui": "^4.0.5",
|
||||||
"chromatic": "^13.1.3",
|
"chromatic": "^13.1.3",
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.23.0",
|
||||||
"eslint-import-resolver-typescript": "^4.2.5",
|
"eslint-import-resolver-typescript": "^4.2.5",
|
||||||
@ -178,24 +177,24 @@
|
|||||||
"eslint-plugin-promise": "~7.2.1",
|
"eslint-plugin-promise": "~7.2.1",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-storybook": "^9.0.4",
|
"eslint-plugin-storybook": "^10.0.2",
|
||||||
"fake-indexeddb": "^6.0.1",
|
"fake-indexeddb": "^6.0.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"lint-staged": "^16.0.0",
|
"lint-staged": "^16.2.6",
|
||||||
"msw": "^2.10.2",
|
"msw": "^2.10.2",
|
||||||
"msw-storybook-addon": "^2.0.5",
|
"msw-storybook-addon": "^2.0.5",
|
||||||
"playwright": "^1.56.1",
|
"playwright": "^1.56.1",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"react-test-renderer": "^18.2.0",
|
"react-test-renderer": "^18.2.0",
|
||||||
"storybook": "^9.1.1",
|
"storybook": "^10.0.2",
|
||||||
"stylelint": "^16.19.1",
|
"stylelint": "^16.19.1",
|
||||||
"stylelint-config-prettier-scss": "^1.0.0",
|
"stylelint-config-prettier-scss": "^1.0.0",
|
||||||
"stylelint-config-standard-scss": "^16.0.0",
|
"stylelint-config-standard-scss": "^16.0.0",
|
||||||
"typescript": "~5.9.0",
|
"typescript": "~5.9.0",
|
||||||
"typescript-eslint": "^8.45.0",
|
"typescript-eslint": "^8.45.0",
|
||||||
"typescript-plugin-css-modules": "^5.2.0",
|
"typescript-plugin-css-modules": "^5.2.0",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^4.0.5"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/react": "^18.2.7",
|
"@types/react": "^18.2.7",
|
||||||
|
|||||||
@ -24,8 +24,8 @@
|
|||||||
"jsdom": "^27.0.0",
|
"jsdom": "^27.0.0",
|
||||||
"pg": "^8.5.0",
|
"pg": "^8.5.0",
|
||||||
"pg-connection-string": "^2.6.0",
|
"pg-connection-string": "^2.6.0",
|
||||||
"pino": "^9.0.0",
|
"pino": "^10.0.0",
|
||||||
"pino-http": "^10.0.0",
|
"pino-http": "^11.0.0",
|
||||||
"prom-client": "^15.0.0",
|
"prom-client": "^15.0.0",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"ws": "^8.12.1"
|
"ws": "^8.12.1"
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin';
|
||||||
|
import { playwright } from '@vitest/browser-playwright';
|
||||||
import {
|
import {
|
||||||
configDefaults,
|
configDefaults,
|
||||||
defineConfig,
|
defineConfig,
|
||||||
@ -23,7 +24,7 @@ const storybookTests: TestProjectInlineConfiguration = {
|
|||||||
browser: {
|
browser: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
headless: true,
|
headless: true,
|
||||||
provider: 'playwright',
|
provider: playwright(),
|
||||||
instances: [{ browser: 'chromium' }],
|
instances: [{ browser: 'chromium' }],
|
||||||
},
|
},
|
||||||
setupFiles: [resolve(__dirname, '.storybook/vitest.setup.ts')],
|
setupFiles: [resolve(__dirname, '.storybook/vitest.setup.ts')],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user