mirror of
https://git.kescher.at/CatCatNya/catstodon.git
synced 2024-11-24 20:58:07 +01:00
Merge branch 'upstream-main' into develop
This commit is contained in:
commit
7fa9c34dee
433 changed files with 5909 additions and 3828 deletions
|
@ -73,6 +73,16 @@ DB_PORT=5432
|
|||
SECRET_KEY_BASE=
|
||||
OTP_SECRET=
|
||||
|
||||
# Encryption secrets
|
||||
# ------------------
|
||||
# Must be available (and set to same values) for all server processes
|
||||
# These are private/secret values, do not share outside hosting environment
|
||||
# Use `bin/rails db:encryption:init` to generate fresh secrets
|
||||
# Do not change these secrets once in use, as this would cause data loss and other issues
|
||||
# ------------------
|
||||
# ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=
|
||||
# ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=
|
||||
# ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=
|
||||
|
||||
# Web Push
|
||||
# --------
|
||||
|
|
13
.github/ISSUE_TEMPLATE/1.web_bug_report.yml
vendored
13
.github/ISSUE_TEMPLATE/1.web_bug_report.yml
vendored
|
@ -1,6 +1,7 @@
|
|||
name: Bug Report (Web Interface)
|
||||
description: If you are using Mastodon's web interface and something is not working as expected
|
||||
labels: [bug, 'status/to triage', 'area/web interface']
|
||||
description: There is a problem using Mastodon's web interface.
|
||||
labels: ['status/to triage', 'area/web interface']
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
@ -47,8 +48,8 @@ body:
|
|||
attributes:
|
||||
label: Mastodon version
|
||||
description: |
|
||||
This is displayed at the bottom of the About page, eg. `v4.1.2+nightly-20230627`
|
||||
placeholder: v4.1.2
|
||||
This is displayed at the bottom of the About page, eg. `v4.4.0-alpha.1`
|
||||
placeholder: v4.3.0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
@ -56,7 +57,7 @@ body:
|
|||
label: Browser name and version
|
||||
description: |
|
||||
What browser are you using when getting this bug? Please specify the version as well.
|
||||
placeholder: Firefox 105.0.3
|
||||
placeholder: Firefox 131.0.0
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
@ -64,7 +65,7 @@ body:
|
|||
label: Operating system
|
||||
description: |
|
||||
What OS are you running? Please specify the version as well.
|
||||
placeholder: macOS 13.4.1
|
||||
placeholder: macOS 15.0.1
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
|
13
.github/ISSUE_TEMPLATE/2.server_bug_report.yml
vendored
13
.github/ISSUE_TEMPLATE/2.server_bug_report.yml
vendored
|
@ -1,7 +1,8 @@
|
|||
name: Bug Report (server / API)
|
||||
description: |
|
||||
If something is not working as expected, but is not from using the web interface.
|
||||
labels: [bug, 'status/to triage']
|
||||
There is a problem with the HTTP server, REST API, ActivityPub interaction, etc.
|
||||
labels: ['status/to triage']
|
||||
type: 'Bug'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
@ -48,8 +49,8 @@ body:
|
|||
attributes:
|
||||
label: Mastodon version
|
||||
description: |
|
||||
This is displayed at the bottom of the About page, eg. `v4.1.2+nightly-20230627`
|
||||
placeholder: v4.1.2
|
||||
This is displayed at the bottom of the About page, eg. `v4.4.0-alpha.1`
|
||||
placeholder: v4.3.0
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
@ -59,7 +60,7 @@ body:
|
|||
Any additional technical details you may have, like logs or error traces
|
||||
value: |
|
||||
If this is happening on your own Mastodon server, please fill out those:
|
||||
- Ruby version: (from `ruby --version`, eg. v3.1.2)
|
||||
- Node.js version: (from `node --version`, eg. v18.16.0)
|
||||
- Ruby version: (from `ruby --version`, eg. v3.3.5)
|
||||
- Node.js version: (from `node --version`, eg. v20.18.0)
|
||||
validations:
|
||||
required: false
|
||||
|
|
74
.github/ISSUE_TEMPLATE/3.troubleshooting.yml
vendored
Normal file
74
.github/ISSUE_TEMPLATE/3.troubleshooting.yml
vendored
Normal file
|
@ -0,0 +1,74 @@
|
|||
name: Deployment troubleshooting
|
||||
description: |
|
||||
You are a server administrator and you are encountering a technical issue during installation, upgrade or operations of Mastodon.
|
||||
labels: ['status/to triage']
|
||||
type: 'Troubleshooting'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Make sure that you are submitting a new bug that was not previously reported or already fixed.
|
||||
|
||||
Please use a concise and distinct title for the issue.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce the problem
|
||||
description: What were you trying to do?
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
...
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Expected behaviour
|
||||
description: What should have happened?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Actual behaviour
|
||||
description: What happened?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Detailed description
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: Mastodon instance
|
||||
description: The address of the Mastodon instance where you experienced the issue
|
||||
placeholder: mastodon.social
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
attributes:
|
||||
label: Mastodon version
|
||||
description: |
|
||||
This is displayed at the bottom of the About page, eg. `v4.4.0-alpha.1`
|
||||
placeholder: v4.3.0
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
Details about your environment, like how Mastodon is deployed, if containers are used, version numbers, etc.
|
||||
value: |
|
||||
Please at least include those informations:
|
||||
- Operating system: (eg. Ubuntu 22.04)
|
||||
- Ruby version: (from `ruby --version`, eg. v3.3.5)
|
||||
- Node.js version: (from `node --version`, eg. v20.18.0)
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Technical details
|
||||
description: |
|
||||
Any additional technical details you may have, like logs or error traces
|
||||
validations:
|
||||
required: false
|
|
@ -1,6 +1,6 @@
|
|||
name: Feature Request
|
||||
description: I have a suggestion
|
||||
labels: [suggestion]
|
||||
type: Suggestion
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
6
.github/workflows/build-push-pr.yml
vendored
6
.github/workflows/build-push-pr.yml
vendored
|
@ -21,9 +21,11 @@ jobs:
|
|||
uses: actions/checkout@v4
|
||||
- id: version_vars
|
||||
run: |
|
||||
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short HEAD) >> $GITHUB_OUTPUT
|
||||
echo mastodon_version_metadata=pr-${{ github.event.pull_request.number }}-$(git rev-parse --short ${{github.event.pull_request.head.sha}}) >> $GITHUB_OUTPUT
|
||||
echo mastodon_short_sha=$(git rev-parse --short ${{github.event.pull_request.head.sha}}) >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
metadata: ${{ steps.version_vars.outputs.mastodon_version_metadata }}
|
||||
short_sha: ${{ steps.version_vars.outputs.mastodon_short_sha }}
|
||||
|
||||
build-image:
|
||||
needs: compute-suffix
|
||||
|
@ -39,6 +41,7 @@ jobs:
|
|||
latest=auto
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
type=ref,event=pr,suffix=-${{ needs.compute-suffix.outputs.short_sha }}
|
||||
secrets: inherit
|
||||
|
||||
build-image-streaming:
|
||||
|
@ -55,4 +58,5 @@ jobs:
|
|||
latest=auto
|
||||
tags: |
|
||||
type=ref,event=pr
|
||||
type=ref,event=pr,suffix=-${{ needs.compute-suffix.outputs.short_sha }}
|
||||
secrets: inherit
|
||||
|
|
2
.github/workflows/build-releases.yml
vendored
2
.github/workflows/build-releases.yml
vendored
|
@ -22,7 +22,7 @@ jobs:
|
|||
# Only tag with latest when ran against the latest stable branch
|
||||
# This needs to be updated after each minor version release
|
||||
flavor: |
|
||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.2.') }}
|
||||
latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
|
||||
tags: |
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
|
|
2
.github/workflows/check-i18n.yml
vendored
2
.github/workflows/check-i18n.yml
vendored
|
@ -18,7 +18,7 @@ permissions:
|
|||
|
||||
jobs:
|
||||
check-i18n:
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
1
.github/workflows/crowdin-upload.yml
vendored
1
.github/workflows/crowdin-upload.yml
vendored
|
@ -1,7 +1,6 @@
|
|||
name: Crowdin / Upload translations
|
||||
|
||||
on:
|
||||
merge_group:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
|
|
2
.github/workflows/test-migrations.yml
vendored
2
.github/workflows/test-migrations.yml
vendored
|
@ -32,6 +32,8 @@ jobs:
|
|||
postgres:
|
||||
- 14-alpine
|
||||
- 15-alpine
|
||||
- 16-alpine
|
||||
- 17-alpine
|
||||
|
||||
services:
|
||||
postgres:
|
||||
|
|
8
.github/workflows/test-ruby.yml
vendored
8
.github/workflows/test-ruby.yml
vendored
|
@ -143,7 +143,7 @@ jobs:
|
|||
uses: ./.github/actions/setup-ruby
|
||||
with:
|
||||
ruby-version: ${{ matrix.ruby-version}}
|
||||
additional-system-dependencies: ffmpeg libpam-dev
|
||||
additional-system-dependencies: ffmpeg imagemagick libpam-dev
|
||||
|
||||
- name: Load database schema
|
||||
run: |
|
||||
|
@ -245,7 +245,7 @@ jobs:
|
|||
uses: ./.github/actions/setup-ruby
|
||||
with:
|
||||
ruby-version: ${{ matrix.ruby-version}}
|
||||
additional-system-dependencies: ffmpeg libpam-dev libyaml-dev
|
||||
additional-system-dependencies: ffmpeg libpam-dev
|
||||
|
||||
- name: Load database schema
|
||||
run: './bin/rails db:create db:schema:load db:seed'
|
||||
|
@ -325,7 +325,7 @@ jobs:
|
|||
uses: ./.github/actions/setup-ruby
|
||||
with:
|
||||
ruby-version: ${{ matrix.ruby-version}}
|
||||
additional-system-dependencies: ffmpeg
|
||||
additional-system-dependencies: ffmpeg imagemagick
|
||||
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
@ -445,7 +445,7 @@ jobs:
|
|||
uses: ./.github/actions/setup-ruby
|
||||
with:
|
||||
ruby-version: ${{ matrix.ruby-version}}
|
||||
additional-system-dependencies: ffmpeg
|
||||
additional-system-dependencies: ffmpeg imagemagick
|
||||
|
||||
- name: Set up Javascript environment
|
||||
uses: ./.github/actions/setup-javascript
|
||||
|
|
|
@ -67,7 +67,7 @@ The following changelog entries focus on changes visible to users, administrator
|
|||
```html
|
||||
<meta name="fediverse:creator" content="username@domain" />
|
||||
```
|
||||
On the API side, this is represented by a new `authors` attribute to the `PreviewCard` entity: https://docs.joinmastodon.org/entities/PreviewCard/#authors\
|
||||
On the API side, this is represented by a new `authors` attribute to the `PreviewCard` entity: https://docs.joinmastodon.org/entities/PreviewCard/#authors \
|
||||
Users can allow arbitrary domains to use `fediverse:creator` to credit them by visiting `/settings/verification`.\
|
||||
This is federated as a new `attributionDomains` property in the `http://joinmastodon.org/ns` namespace, containing an array of domain names: https://docs.joinmastodon.org/spec/activitypub/#properties-used-1
|
||||
- **Add in-app notifications for moderation actions and warnings** (#30065, #30082, and #30081 by @ClearlyClaire)\
|
||||
|
|
|
@ -191,7 +191,7 @@ FROM build AS libvips
|
|||
|
||||
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
|
||||
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips
|
||||
ARG VIPS_VERSION=8.15.3
|
||||
ARG VIPS_VERSION=8.15.5
|
||||
# libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"]
|
||||
ARG VIPS_URL=https://github.com/libvips/libvips/releases/download
|
||||
|
||||
|
|
6
Gemfile
6
Gemfile
|
@ -61,7 +61,7 @@ gem 'irb', '~> 1.8'
|
|||
gem 'kaminari', '~> 1.2'
|
||||
gem 'link_header', '~> 0.0'
|
||||
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
|
||||
gem 'mime-types', '~> 3.5.0', require: 'mime/types/columnar'
|
||||
gem 'mime-types', '~> 3.6.0', require: 'mime/types/columnar'
|
||||
gem 'nokogiri', '~> 1.15'
|
||||
gem 'oj', '~> 3.14'
|
||||
gem 'ox', '~> 2.14'
|
||||
|
@ -111,8 +111,8 @@ group :opentelemetry do
|
|||
gem 'opentelemetry-instrumentation-http_client', '~> 0.22.3', require: false
|
||||
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
|
||||
gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false
|
||||
gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rack', '~> 0.25.0', require: false
|
||||
gem 'opentelemetry-instrumentation-rails', '~> 0.32.0', require: false
|
||||
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
|
||||
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
|
||||
gem 'opentelemetry-sdk', '~> 1.4', require: false
|
||||
|
|
163
Gemfile.lock
163
Gemfile.lock
|
@ -10,35 +10,35 @@ GIT
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
actioncable (7.1.4)
|
||||
actionpack (= 7.1.4)
|
||||
activesupport (= 7.1.4)
|
||||
actioncable (7.1.4.1)
|
||||
actionpack (= 7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (7.1.4)
|
||||
actionpack (= 7.1.4)
|
||||
activejob (= 7.1.4)
|
||||
activerecord (= 7.1.4)
|
||||
activestorage (= 7.1.4)
|
||||
activesupport (= 7.1.4)
|
||||
actionmailbox (7.1.4.1)
|
||||
actionpack (= 7.1.4.1)
|
||||
activejob (= 7.1.4.1)
|
||||
activerecord (= 7.1.4.1)
|
||||
activestorage (= 7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
mail (>= 2.7.1)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
actionmailer (7.1.4)
|
||||
actionpack (= 7.1.4)
|
||||
actionview (= 7.1.4)
|
||||
activejob (= 7.1.4)
|
||||
activesupport (= 7.1.4)
|
||||
actionmailer (7.1.4.1)
|
||||
actionpack (= 7.1.4.1)
|
||||
actionview (= 7.1.4.1)
|
||||
activejob (= 7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (7.1.4)
|
||||
actionview (= 7.1.4)
|
||||
activesupport (= 7.1.4)
|
||||
actionpack (7.1.4.1)
|
||||
actionview (= 7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
racc
|
||||
rack (>= 2.2.4)
|
||||
|
@ -46,15 +46,15 @@ GEM
|
|||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
actiontext (7.1.4)
|
||||
actionpack (= 7.1.4)
|
||||
activerecord (= 7.1.4)
|
||||
activestorage (= 7.1.4)
|
||||
activesupport (= 7.1.4)
|
||||
actiontext (7.1.4.1)
|
||||
actionpack (= 7.1.4.1)
|
||||
activerecord (= 7.1.4.1)
|
||||
activestorage (= 7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (7.1.4)
|
||||
activesupport (= 7.1.4)
|
||||
actionview (7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
|
@ -64,22 +64,22 @@ GEM
|
|||
activemodel (>= 4.1)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (7.1.4)
|
||||
activesupport (= 7.1.4)
|
||||
activejob (7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (7.1.4)
|
||||
activesupport (= 7.1.4)
|
||||
activerecord (7.1.4)
|
||||
activemodel (= 7.1.4)
|
||||
activesupport (= 7.1.4)
|
||||
activemodel (7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
activerecord (7.1.4.1)
|
||||
activemodel (= 7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (7.1.4)
|
||||
actionpack (= 7.1.4)
|
||||
activejob (= 7.1.4)
|
||||
activerecord (= 7.1.4)
|
||||
activesupport (= 7.1.4)
|
||||
activestorage (7.1.4.1)
|
||||
actionpack (= 7.1.4.1)
|
||||
activejob (= 7.1.4.1)
|
||||
activerecord (= 7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
marcel (~> 1.0)
|
||||
activesupport (7.1.4)
|
||||
activesupport (7.1.4.1)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
|
@ -100,17 +100,17 @@ GEM
|
|||
attr_required (1.0.2)
|
||||
awrence (1.2.1)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.983.0)
|
||||
aws-sdk-core (3.209.1)
|
||||
aws-partitions (1.992.0)
|
||||
aws-sdk-core (3.210.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.94.0)
|
||||
aws-sdk-core (~> 3, >= 3.207.0)
|
||||
aws-sdk-kms (1.95.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.167.0)
|
||||
aws-sdk-core (~> 3, >= 3.207.0)
|
||||
aws-sdk-s3 (1.169.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.0)
|
||||
|
@ -137,7 +137,7 @@ GEM
|
|||
blurhash (0.1.8)
|
||||
bootsnap (1.18.4)
|
||||
msgpack (~> 1.2)
|
||||
brakeman (6.2.1)
|
||||
brakeman (6.2.2)
|
||||
racc
|
||||
browser (5.3.1)
|
||||
brpoplpush-redis_script (0.1.3)
|
||||
|
@ -233,7 +233,7 @@ GEM
|
|||
tzinfo
|
||||
excon (0.111.0)
|
||||
fabrication (2.31.0)
|
||||
faker (3.4.2)
|
||||
faker (3.5.1)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (1.10.3)
|
||||
faraday-em_http (~> 1.0)
|
||||
|
@ -429,9 +429,10 @@ GEM
|
|||
azure-storage-blob (~> 2.0.1)
|
||||
hashie (~> 5.0)
|
||||
memory_profiler (1.1.0)
|
||||
mime-types (3.5.2)
|
||||
mime-types (3.6.0)
|
||||
logger
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2024.0820)
|
||||
mime-types-data (3.2024.1001)
|
||||
mini_mime (1.1.5)
|
||||
mini_portile2 (2.8.7)
|
||||
minitest (5.25.1)
|
||||
|
@ -503,7 +504,7 @@ GEM
|
|||
opentelemetry-semantic_conventions
|
||||
opentelemetry-helpers-sql-obfuscation (0.2.0)
|
||||
opentelemetry-common (~> 0.21)
|
||||
opentelemetry-instrumentation-action_mailer (0.1.0)
|
||||
opentelemetry-instrumentation-action_mailer (0.2.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.1)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
|
@ -515,13 +516,13 @@ GEM
|
|||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.1)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-active_job (0.7.7)
|
||||
opentelemetry-instrumentation-active_job (0.7.8)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-active_model_serializers (0.20.2)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-active_record (0.7.3)
|
||||
opentelemetry-instrumentation-active_record (0.8.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-active_support (0.6.0)
|
||||
|
@ -553,16 +554,16 @@ GEM
|
|||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-helpers-sql-obfuscation
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-rack (0.24.6)
|
||||
opentelemetry-instrumentation-rack (0.25.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-rails (0.31.2)
|
||||
opentelemetry-instrumentation-rails (0.32.0)
|
||||
opentelemetry-api (~> 1.0)
|
||||
opentelemetry-instrumentation-action_mailer (~> 0.1.0)
|
||||
opentelemetry-instrumentation-action_mailer (~> 0.2.0)
|
||||
opentelemetry-instrumentation-action_pack (~> 0.9.0)
|
||||
opentelemetry-instrumentation-action_view (~> 0.7.0)
|
||||
opentelemetry-instrumentation-active_job (~> 0.7.0)
|
||||
opentelemetry-instrumentation-active_record (~> 0.7.0)
|
||||
opentelemetry-instrumentation-active_record (~> 0.8.0)
|
||||
opentelemetry-instrumentation-active_support (~> 0.6.0)
|
||||
opentelemetry-instrumentation-base (~> 0.22.1)
|
||||
opentelemetry-instrumentation-redis (0.25.7)
|
||||
|
@ -590,8 +591,8 @@ GEM
|
|||
parslet (2.0.0)
|
||||
pastel (0.8.0)
|
||||
tty-color (~> 0.5)
|
||||
pg (1.5.8)
|
||||
pghero (3.6.0)
|
||||
pg (1.5.9)
|
||||
pghero (3.6.1)
|
||||
activerecord (>= 6.1)
|
||||
premailer (1.27.0)
|
||||
addressable
|
||||
|
@ -615,7 +616,7 @@ GEM
|
|||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.8.1)
|
||||
rack (2.2.9)
|
||||
rack (2.2.10)
|
||||
rack-attack (6.7.0)
|
||||
rack (>= 1.0, < 4)
|
||||
rack-cors (2.0.2)
|
||||
|
@ -638,20 +639,20 @@ GEM
|
|||
rackup (1.0.0)
|
||||
rack (< 3)
|
||||
webrick
|
||||
rails (7.1.4)
|
||||
actioncable (= 7.1.4)
|
||||
actionmailbox (= 7.1.4)
|
||||
actionmailer (= 7.1.4)
|
||||
actionpack (= 7.1.4)
|
||||
actiontext (= 7.1.4)
|
||||
actionview (= 7.1.4)
|
||||
activejob (= 7.1.4)
|
||||
activemodel (= 7.1.4)
|
||||
activerecord (= 7.1.4)
|
||||
activestorage (= 7.1.4)
|
||||
activesupport (= 7.1.4)
|
||||
rails (7.1.4.1)
|
||||
actioncable (= 7.1.4.1)
|
||||
actionmailbox (= 7.1.4.1)
|
||||
actionmailer (= 7.1.4.1)
|
||||
actionpack (= 7.1.4.1)
|
||||
actiontext (= 7.1.4.1)
|
||||
actionview (= 7.1.4.1)
|
||||
activejob (= 7.1.4.1)
|
||||
activemodel (= 7.1.4.1)
|
||||
activerecord (= 7.1.4.1)
|
||||
activestorage (= 7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 7.1.4)
|
||||
railties (= 7.1.4.1)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
actionview (>= 5.0.1.rc1)
|
||||
|
@ -666,9 +667,9 @@ GEM
|
|||
rails-i18n (7.0.9)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 8)
|
||||
railties (7.1.4)
|
||||
actionpack (= 7.1.4)
|
||||
activesupport (= 7.1.4)
|
||||
railties (7.1.4.1)
|
||||
actionpack (= 7.1.4.1)
|
||||
activesupport (= 7.1.4.1)
|
||||
irb
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
|
@ -761,7 +762,7 @@ GEM
|
|||
rubocop-rspec_rails (2.30.0)
|
||||
rubocop (~> 1.61)
|
||||
rubocop-rspec (~> 3, >= 3.0.1)
|
||||
ruby-prof (1.7.0)
|
||||
ruby-prof (1.7.1)
|
||||
ruby-progressbar (1.13.0)
|
||||
ruby-saml (1.17.0)
|
||||
nokogiri (>= 1.13.10)
|
||||
|
@ -822,7 +823,7 @@ GEM
|
|||
stoplight (4.1.0)
|
||||
redlock (~> 1.0)
|
||||
stringio (3.1.1)
|
||||
strong_migrations (2.0.0)
|
||||
strong_migrations (2.0.1)
|
||||
activerecord (>= 6.1)
|
||||
swd (1.3.0)
|
||||
activesupport (>= 3)
|
||||
|
@ -970,7 +971,7 @@ DEPENDENCIES
|
|||
mario-redis-lock (~> 1.2)
|
||||
md-paperclip-azure (~> 2.2)
|
||||
memory_profiler
|
||||
mime-types (~> 3.5.0)
|
||||
mime-types (~> 3.6.0)
|
||||
net-http (~> 0.4.0)
|
||||
net-ldap (~> 0.18)
|
||||
nokogiri (~> 1.15)
|
||||
|
@ -991,8 +992,8 @@ DEPENDENCIES
|
|||
opentelemetry-instrumentation-http_client (~> 0.22.3)
|
||||
opentelemetry-instrumentation-net_http (~> 0.22.4)
|
||||
opentelemetry-instrumentation-pg (~> 0.29.0)
|
||||
opentelemetry-instrumentation-rack (~> 0.24.1)
|
||||
opentelemetry-instrumentation-rails (~> 0.31.0)
|
||||
opentelemetry-instrumentation-rack (~> 0.25.0)
|
||||
opentelemetry-instrumentation-rails (~> 0.32.0)
|
||||
opentelemetry-instrumentation-redis (~> 0.25.3)
|
||||
opentelemetry-instrumentation-sidekiq (~> 0.25.2)
|
||||
opentelemetry-sdk (~> 1.4)
|
||||
|
@ -1057,7 +1058,7 @@ DEPENDENCIES
|
|||
xorcist (~> 1.1)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.3.4p94
|
||||
ruby 3.3.5p100
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.18
|
||||
2.5.22
|
||||
|
|
|
@ -52,7 +52,7 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
|
|||
private
|
||||
|
||||
def load_requests
|
||||
requests = NotificationRequest.where(account: current_account).includes(:last_status, from_account: [:account_stat, :user]).to_a_paginated_by_id(
|
||||
requests = NotificationRequest.where(account: current_account).without_suspended.includes(:last_status, from_account: [:account_stat, :user]).to_a_paginated_by_id(
|
||||
limit_param(DEFAULT_ACCOUNTS_LIMIT),
|
||||
params_slice(:max_id, :since_id, :min_id)
|
||||
)
|
||||
|
|
|
@ -23,6 +23,6 @@ class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseControl
|
|||
private
|
||||
|
||||
def set_translation
|
||||
@translation = TranslateStatusService.new.call(@status, content_locale)
|
||||
@translation = TranslateStatusService.new.call(@status, I18n.locale.to_s)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
||||
before_action :require_user!
|
||||
before_action :require_user!, except: :destroy
|
||||
before_action :set_push_subscription, only: :update
|
||||
before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions?
|
||||
after_action :update_session_with_subscription, only: :create
|
||||
|
@ -17,6 +17,13 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
|
|||
render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||
end
|
||||
|
||||
def destroy
|
||||
push_subscription = ::Web::PushSubscription.find_by_token_for(:unsubscribe, params[:id])
|
||||
push_subscription&.destroy
|
||||
|
||||
head 200
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def active_session
|
||||
|
|
|
@ -35,7 +35,7 @@ class ApplicationController < ActionController::Base
|
|||
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
|
||||
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests
|
||||
|
||||
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
|
||||
rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error)
|
||||
rescue_from Mastodon::RaceConditionError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable
|
||||
|
||||
rescue_from Seahorse::Client::NetworkingError do |e|
|
||||
|
|
|
@ -20,7 +20,7 @@ module Api::ErrorHandling
|
|||
render json: { error: 'Record not found' }, status: 404
|
||||
end
|
||||
|
||||
rescue_from HTTP::Error, Mastodon::UnexpectedResponseError do
|
||||
rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, Mastodon::UnexpectedResponseError) do
|
||||
render json: { error: 'Remote data could not be fetched' }, status: 503
|
||||
end
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ module Auth::CaptchaConcern
|
|||
end
|
||||
|
||||
def captcha_available?
|
||||
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
|
||||
Rails.configuration.x.captcha.secret_key.present? && Rails.configuration.x.captcha.site_key.present?
|
||||
end
|
||||
|
||||
def captcha_enabled?
|
||||
|
|
|
@ -80,7 +80,7 @@ module SignatureVerification
|
|||
fail_with! "Verification failed for #{actor.to_log_human_identifier} #{actor.uri} using rsa-sha256 (RSASSA-PKCS1-v1_5 with SHA-256)", signed_string: compare_signed_string, signature: signature_params['signature']
|
||||
rescue SignatureVerificationError => e
|
||||
fail_with! e.message
|
||||
rescue HTTP::Error, OpenSSL::SSL::SSLError => e
|
||||
rescue *Mastodon::HTTP_CONNECTION_ERRORS => e
|
||||
fail_with! "Failed to fetch remote data: #{e.message}"
|
||||
rescue Mastodon::UnexpectedResponseError
|
||||
fail_with! 'Failed to fetch remote data (got unexpected reply from server)'
|
||||
|
|
|
@ -13,7 +13,7 @@ class MediaProxyController < ApplicationController
|
|||
rescue_from ActiveRecord::RecordInvalid, with: :not_found
|
||||
rescue_from Mastodon::UnexpectedResponseError, with: :not_found
|
||||
rescue_from Mastodon::NotPermittedError, with: :not_found
|
||||
rescue_from HTTP::TimeoutError, HTTP::ConnectionError, OpenSSL::SSL::SSLError, with: :internal_server_error
|
||||
rescue_from(*Mastodon::HTTP_CONNECTION_ERRORS, with: :internal_server_error)
|
||||
|
||||
def show
|
||||
with_redis_lock("media_download:#{params[:id]}") do
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module Admin::SettingsHelper
|
||||
def captcha_available?
|
||||
ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present?
|
||||
Rails.configuration.x.captcha.secret_key.present? && Rails.configuration.x.captcha.site_key.present?
|
||||
end
|
||||
|
||||
def login_activity_title(activity)
|
||||
|
|
|
@ -120,18 +120,6 @@ module ApplicationHelper
|
|||
inline_svg_tag 'check.svg'
|
||||
end
|
||||
|
||||
def visibility_icon(status)
|
||||
if status.public_visibility?
|
||||
material_symbol('globe', title: I18n.t('statuses.visibilities.public'))
|
||||
elsif status.unlisted_visibility?
|
||||
material_symbol('lock_open', title: I18n.t('statuses.visibilities.unlisted'))
|
||||
elsif status.private_visibility? || status.limited_visibility?
|
||||
material_symbol('lock', title: I18n.t('statuses.visibilities.private'))
|
||||
elsif status.direct_visibility?
|
||||
material_symbol('alternate_email', title: I18n.t('statuses.visibilities.direct'))
|
||||
end
|
||||
end
|
||||
|
||||
def interrelationships_icon(relationships, account_id)
|
||||
if relationships.following[account_id] && relationships.followed_by[account_id]
|
||||
material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive')
|
||||
|
@ -245,6 +233,11 @@ module ApplicationHelper
|
|||
tag.input(type: :text, maxlength: 999, spellcheck: false, readonly: true, **options)
|
||||
end
|
||||
|
||||
def recent_tag_usage(tag)
|
||||
people = tag.history.aggregate(2.days.ago.to_date..Time.zone.today).accounts
|
||||
I18n.t 'user_mailer.welcome.hashtags_recent_count', people: number_with_delimiter(people), count: people
|
||||
end
|
||||
|
||||
# glitch-soc addition to handle the multiple flavors
|
||||
def preload_locale_pack
|
||||
supported_locales = Themes.instance.flavour(current_flavour)['locales']
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module FormattingHelper
|
||||
SYNDICATED_EMOJI_STYLES = <<~CSS.squish
|
||||
height: 1.1em;
|
||||
margin: -.2ex .15em .2ex;
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
width: 1.1em;
|
||||
CSS
|
||||
|
||||
def html_aware_format(text, local, options = {})
|
||||
HtmlAwareFormatter.new(text, local, options).to_s
|
||||
end
|
||||
|
@ -23,33 +31,10 @@ module FormattingHelper
|
|||
end
|
||||
|
||||
def rss_status_content_format(status)
|
||||
html = status_content_format(status)
|
||||
|
||||
before_html = if status.spoiler_text?
|
||||
tag.p do
|
||||
tag.strong do
|
||||
I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale)
|
||||
end
|
||||
|
||||
status.spoiler_text
|
||||
end + tag.hr
|
||||
end
|
||||
|
||||
after_html = if status.preloadable_poll
|
||||
tag.p do
|
||||
safe_join(
|
||||
status.preloadable_poll.options.map do |o|
|
||||
tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', o, disabled: true)
|
||||
end,
|
||||
tag.br
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
prerender_custom_emojis(
|
||||
safe_join([before_html, html, after_html]),
|
||||
wrapped_status_content_format(status),
|
||||
status.emojis,
|
||||
style: 'width: 1.1em; height: 1.1em; object-fit: contain; vertical-align: middle; margin: -.2ex .15em .2ex'
|
||||
style: SYNDICATED_EMOJI_STYLES
|
||||
).to_str
|
||||
end
|
||||
|
||||
|
@ -64,4 +49,47 @@ module FormattingHelper
|
|||
html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def wrapped_status_content_format(status)
|
||||
safe_join [
|
||||
rss_content_preroll(status),
|
||||
status_content_format(status),
|
||||
rss_content_postroll(status),
|
||||
]
|
||||
end
|
||||
|
||||
def rss_content_preroll(status)
|
||||
if status.spoiler_text?
|
||||
safe_join [
|
||||
tag.p { spoiler_with_warning(status) },
|
||||
tag.hr,
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
def spoiler_with_warning(status)
|
||||
safe_join [
|
||||
tag.strong { I18n.t('rss.content_warning', locale: available_locale_or_nil(status.language) || I18n.default_locale) },
|
||||
status.spoiler_text,
|
||||
]
|
||||
end
|
||||
|
||||
def rss_content_postroll(status)
|
||||
if status.preloadable_poll
|
||||
tag.p do
|
||||
poll_option_tags(status)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def poll_option_tags(status)
|
||||
safe_join(
|
||||
status.preloadable_poll.options.map do |option|
|
||||
tag.send(status.preloadable_poll.multiple? ? 'checkbox' : 'radio', option, disabled: true)
|
||||
end,
|
||||
tag.br
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -162,7 +162,7 @@ module LanguagesHelper
|
|||
th: ['Thai', 'ไทย'].freeze,
|
||||
ti: ['Tigrinya', 'ትግርኛ'].freeze,
|
||||
tk: ['Turkmen', 'Türkmen'].freeze,
|
||||
tl: ['Tagalog', 'Wikang Tagalog'].freeze,
|
||||
tl: ['Tagalog', 'Tagalog'].freeze,
|
||||
tn: ['Tswana', 'Setswana'].freeze,
|
||||
to: ['Tonga', 'faka Tonga'].freeze,
|
||||
tr: ['Turkish', 'Türkçe'].freeze,
|
||||
|
@ -193,6 +193,7 @@ module LanguagesHelper
|
|||
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
|
||||
cnr: ['Montenegrin', 'crnogorski'].freeze,
|
||||
csb: ['Kashubian', 'Kaszëbsczi'].freeze,
|
||||
gsw: ['Swiss German', 'Schwiizertütsch'].freeze,
|
||||
jbo: ['Lojban', 'la .lojban.'].freeze,
|
||||
kab: ['Kabyle', 'Taqbaylit'].freeze,
|
||||
ldn: ['Láadan', 'Láadan'].freeze,
|
||||
|
|
|
@ -12,7 +12,7 @@ module StatusesHelper
|
|||
}.freeze
|
||||
|
||||
def nothing_here(extra_classes = '')
|
||||
content_tag(:div, class: "nothing-here #{extra_classes}") do
|
||||
tag.div(class: ['nothing-here', extra_classes]) do
|
||||
t('accounts.nothing_here')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -327,31 +327,24 @@ Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
|
|||
|
||||
if (!input) return;
|
||||
|
||||
const oldReadOnly = input.readOnly;
|
||||
|
||||
input.readOnly = false;
|
||||
input.focus();
|
||||
input.select();
|
||||
input.setSelectionRange(0, input.value.length);
|
||||
|
||||
try {
|
||||
if (document.execCommand('copy')) {
|
||||
input.blur();
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(input.value)
|
||||
.then(() => {
|
||||
const parent = target.parentElement;
|
||||
|
||||
if (!parent) return;
|
||||
if (parent) {
|
||||
parent.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
parent.classList.remove('copied');
|
||||
}, 700);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
input.readOnly = oldReadOnly;
|
||||
return true;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
const toggleSidebar = () => {
|
||||
|
|
|
@ -8,6 +8,7 @@ import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
|
|||
import type {
|
||||
ApiNotificationGroupJSON,
|
||||
ApiNotificationJSON,
|
||||
NotificationType,
|
||||
} from 'flavours/glitch/api_types/notifications';
|
||||
import { allNotificationTypes } from 'flavours/glitch/api_types/notifications';
|
||||
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses';
|
||||
|
@ -15,6 +16,7 @@ import { usePendingItems } from 'flavours/glitch/initial_state';
|
|||
import type { NotificationGap } from 'flavours/glitch/reducers/notification_groups';
|
||||
import {
|
||||
selectSettingsNotificationsExcludedTypes,
|
||||
selectSettingsNotificationsGroupFollows,
|
||||
selectSettingsNotificationsQuickFilterActive,
|
||||
selectSettingsNotificationsShows,
|
||||
} from 'flavours/glitch/selectors/settings';
|
||||
|
@ -68,17 +70,19 @@ function dispatchAssociatedRecords(
|
|||
dispatch(importFetchedStatuses(fetchedStatuses));
|
||||
}
|
||||
|
||||
const supportedGroupedNotificationTypes = ['favourite', 'reblog'];
|
||||
function selectNotificationGroupedTypes(state: RootState) {
|
||||
const types: NotificationType[] = ['favourite', 'reblog'];
|
||||
|
||||
export function shouldGroupNotificationType(type: string) {
|
||||
return supportedGroupedNotificationTypes.includes(type);
|
||||
if (selectSettingsNotificationsGroupFollows(state)) types.push('follow');
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
export const fetchNotifications = createDataLoadingThunk(
|
||||
'notificationGroups/fetch',
|
||||
async (_params, { getState }) =>
|
||||
apiFetchNotificationGroups({
|
||||
grouped_types: supportedGroupedNotificationTypes,
|
||||
grouped_types: selectNotificationGroupedTypes(getState()),
|
||||
exclude_types: getExcludedTypes(getState()),
|
||||
}),
|
||||
({ notifications, accounts, statuses }, { dispatch }) => {
|
||||
|
@ -102,7 +106,7 @@ export const fetchNotificationsGap = createDataLoadingThunk(
|
|||
'notificationGroups/fetchGap',
|
||||
async (params: { gap: NotificationGap }, { getState }) =>
|
||||
apiFetchNotificationGroups({
|
||||
grouped_types: supportedGroupedNotificationTypes,
|
||||
grouped_types: selectNotificationGroupedTypes(getState()),
|
||||
max_id: params.gap.maxId,
|
||||
exclude_types: getExcludedTypes(getState()),
|
||||
}),
|
||||
|
@ -119,7 +123,7 @@ export const pollRecentNotifications = createDataLoadingThunk(
|
|||
'notificationGroups/pollRecentNotifications',
|
||||
async (_params, { getState }) => {
|
||||
return apiFetchNotificationGroups({
|
||||
grouped_types: supportedGroupedNotificationTypes,
|
||||
grouped_types: selectNotificationGroupedTypes(getState()),
|
||||
max_id: undefined,
|
||||
exclude_types: getExcludedTypes(getState()),
|
||||
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
|
||||
|
@ -168,7 +172,10 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
|
|||
|
||||
dispatchAssociatedRecords(dispatch, [notification]);
|
||||
|
||||
return notification;
|
||||
return {
|
||||
notification,
|
||||
groupedTypes: selectNotificationGroupedTypes(state),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ export interface ApiAccountRoleJSON {
|
|||
}
|
||||
|
||||
// See app/serializers/rest/account_serializer.rb
|
||||
export interface ApiAccountJSON {
|
||||
export interface BaseApiAccountJSON {
|
||||
acct: string;
|
||||
avatar: string;
|
||||
avatar_static: string;
|
||||
|
@ -45,3 +45,12 @@ export interface ApiAccountJSON {
|
|||
memorial?: boolean;
|
||||
hide_collections: boolean;
|
||||
}
|
||||
|
||||
// See app/serializers/rest/muted_account_serializer.rb
|
||||
export interface ApiMutedAccountJSON extends BaseApiAccountJSON {
|
||||
mute_expires_at?: string | null;
|
||||
}
|
||||
|
||||
// For now, we have the same type representing both `Account` and `MutedAccount`
|
||||
// objects, but we should refactor this in the future.
|
||||
export type ApiAccountJSON = ApiMutedAccountJSON;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { PropsWithChildren } from 'react';
|
||||
import type { PropsWithChildren, JSX } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
/* Significantly rewritten from upstream to keep the old design for now */
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { StatusBanner, BannerVariant } from './status_banner';
|
||||
|
||||
export const ContentWarning: React.FC<{
|
||||
text: string;
|
||||
|
@ -8,20 +6,12 @@ export const ContentWarning: React.FC<{
|
|||
onClick?: () => void;
|
||||
icons?: React.ReactNode[];
|
||||
}> = ({ text, expanded, onClick, icons }) => (
|
||||
<p>
|
||||
<span dangerouslySetInnerHTML={{ __html: text }} className='translate' />{' '}
|
||||
<button
|
||||
type='button'
|
||||
className='status__content__spoiler-link'
|
||||
<StatusBanner
|
||||
expanded={expanded}
|
||||
onClick={onClick}
|
||||
aria-expanded={expanded}
|
||||
variant={BannerVariant.Warning}
|
||||
>
|
||||
{expanded ? (
|
||||
<FormattedMessage id='status.show_less' defaultMessage='Show less' />
|
||||
) : (
|
||||
<FormattedMessage id='status.show_more' defaultMessage='Show more' />
|
||||
)}
|
||||
{icons}
|
||||
</button>
|
||||
</p>
|
||||
<p dangerouslySetInnerHTML={{ __html: text }} />
|
||||
</StatusBanner>
|
||||
);
|
||||
|
|
|
@ -10,13 +10,16 @@ export const FilterWarning: React.FC<{
|
|||
<StatusBanner
|
||||
expanded={expanded}
|
||||
onClick={onClick}
|
||||
variant={BannerVariant.Blue}
|
||||
variant={BannerVariant.Filter}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='filter_warning.matches_filter'
|
||||
defaultMessage='Matches filter “{title}”'
|
||||
values={{ title }}
|
||||
defaultMessage='Matches filter “<span>{title}</span>”'
|
||||
values={{
|
||||
title,
|
||||
span: (chunks) => <span className='filter-name'>{chunks}</span>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</StatusBanner>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { memo } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||
|
||||
|
|
|
@ -654,7 +654,7 @@ class Status extends ImmutablePureComponent {
|
|||
media={status.get('media_attachments')}
|
||||
/>,
|
||||
);
|
||||
} else if (['image', 'gifv'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
|
||||
} else if (['image', 'gifv', 'unknown'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
|
||||
media.push(
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||
{Component => (
|
||||
|
|
|
@ -241,7 +241,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
|
||||
}
|
||||
|
||||
if (publicStatus && (signedIn || !isRemote)) {
|
||||
if (publicStatus && !isRemote) {
|
||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export enum BannerVariant {
|
||||
Yellow = 'yellow',
|
||||
Blue = 'blue',
|
||||
Warning = 'warning',
|
||||
Filter = 'filter',
|
||||
}
|
||||
|
||||
export const StatusBanner: React.FC<{
|
||||
|
@ -11,9 +11,9 @@ export const StatusBanner: React.FC<{
|
|||
expanded?: boolean;
|
||||
onClick?: () => void;
|
||||
}> = ({ children, variant, expanded, onClick }) => (
|
||||
<div
|
||||
<label
|
||||
className={
|
||||
variant === BannerVariant.Yellow
|
||||
variant === BannerVariant.Warning
|
||||
? 'content-warning'
|
||||
: 'content-warning content-warning--filter'
|
||||
}
|
||||
|
@ -26,6 +26,11 @@ export const StatusBanner: React.FC<{
|
|||
id='content_warning.hide'
|
||||
defaultMessage='Hide post'
|
||||
/>
|
||||
) : variant === BannerVariant.Warning ? (
|
||||
<FormattedMessage
|
||||
id='content_warning.show_more'
|
||||
defaultMessage='Show more'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='content_warning.show'
|
||||
|
@ -33,5 +38,5 @@ export const StatusBanner: React.FC<{
|
|||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
|
|
|
@ -378,7 +378,7 @@ class StatusContent extends PureComponent {
|
|||
)).reduce((aggregate, item) => [...aggregate, item, ' '], []);
|
||||
|
||||
let spoilerIcons = [];
|
||||
if (hidden && mediaIcons) {
|
||||
if (mediaIcons) {
|
||||
const mediaComponents = {
|
||||
'link': LinkIcon,
|
||||
'picture-o': ImageIcon,
|
||||
|
|
|
@ -327,31 +327,24 @@ Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
|
|||
|
||||
if (!input) return;
|
||||
|
||||
const oldReadOnly = input.readOnly;
|
||||
|
||||
input.readOnly = false;
|
||||
input.focus();
|
||||
input.select();
|
||||
input.setSelectionRange(0, input.value.length);
|
||||
|
||||
try {
|
||||
if (document.execCommand('copy')) {
|
||||
input.blur();
|
||||
|
||||
navigator.clipboard
|
||||
.writeText(input.value)
|
||||
.then(() => {
|
||||
const parent = target.parentElement;
|
||||
|
||||
if (!parent) return;
|
||||
if (parent) {
|
||||
parent.classList.add('copied');
|
||||
|
||||
setTimeout(() => {
|
||||
parent.classList.remove('copied');
|
||||
}, 700);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
input.readOnly = oldReadOnly;
|
||||
return true;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
const toggleSidebar = () => {
|
||||
|
|
|
@ -27,15 +27,19 @@ class ColumnSettings extends PureComponent {
|
|||
|
||||
return (
|
||||
<div className='column-settings'>
|
||||
<section>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ const messages = defineMessages({
|
|||
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
||||
singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Pick one' },
|
||||
singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Single choice' },
|
||||
multipleChoice: { id: 'compose_form.poll.multiple', defaultMessage: 'Multiple choice' },
|
||||
});
|
||||
|
||||
|
|
|
@ -129,8 +129,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
|
||||
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
|
||||
setCanScrollRight(bodyRef.current.scrollLeft < 0);
|
||||
} else {
|
||||
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
||||
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
||||
}
|
||||
}, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
|
||||
|
||||
const handleLeftNav = useCallback(() => {
|
||||
|
@ -146,8 +151,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
|
||||
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
|
||||
setCanScrollRight(bodyRef.current.scrollLeft < 0);
|
||||
} else {
|
||||
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
||||
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
||||
}
|
||||
}, [setCanScrollRight, setCanScrollLeft, bodyRef]);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
|
|
|
@ -40,6 +40,7 @@ class ColumnSettings extends PureComponent {
|
|||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
||||
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
||||
const groupStr = <FormattedMessage id='notifications.column_settings.group' defaultMessage='Group' />;
|
||||
|
||||
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
|
||||
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
||||
|
@ -96,6 +97,10 @@ class ColumnSettings extends PureComponent {
|
|||
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
|
||||
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
|
||||
</div>
|
||||
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['group', 'follow']} onChange={onChange} label={groupStr} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section role='group' aria-labelledby='notifications-follow-request'>
|
||||
|
|
|
@ -56,11 +56,12 @@ const mapDispatchToProps = (dispatch) => ({
|
|||
} else {
|
||||
dispatch(changeSetting(['notifications', ...path], checked));
|
||||
}
|
||||
} else if(path[0] === 'groupingBeta') {
|
||||
dispatch(changeSetting(['notifications', ...path], checked));
|
||||
dispatch(initializeNotifications());
|
||||
} else {
|
||||
dispatch(changeSetting(['notifications', ...path], checked));
|
||||
|
||||
if(path[0] === 'group' && path[1] === 'follow') {
|
||||
dispatch(initializeNotifications());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
import type { JSX } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
|
||||
import { FollowersCounter } from 'flavours/glitch/components/counters';
|
||||
import { FollowButton } from 'flavours/glitch/components/follow_button';
|
||||
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
import type { NotificationGroupFollow } from 'flavours/glitch/models/notification_group';
|
||||
import { useAppSelector } from 'flavours/glitch/store';
|
||||
|
||||
import type { LabelRenderer } from './notification_group_with_status';
|
||||
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
||||
|
||||
const labelRenderer: LabelRenderer = (displayedName, total) => {
|
||||
const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => {
|
||||
if (total === 1)
|
||||
return (
|
||||
<FormattedMessage
|
||||
|
@ -23,10 +28,12 @@ const labelRenderer: LabelRenderer = (displayedName, total) => {
|
|||
return (
|
||||
<FormattedMessage
|
||||
id='notification.follow.name_and_others'
|
||||
defaultMessage='{name} and {count, plural, one {# other} other {# others}} followed you'
|
||||
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a> followed you'
|
||||
values={{
|
||||
name: displayedName,
|
||||
count: total - 1,
|
||||
a: (chunks) =>
|
||||
seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -46,6 +53,10 @@ export const NotificationFollow: React.FC<{
|
|||
notification: NotificationGroupFollow;
|
||||
unread: boolean;
|
||||
}> = ({ notification, unread }) => {
|
||||
const username = useAppSelector(
|
||||
(state) => state.accounts.getIn([me, 'username']) as string,
|
||||
);
|
||||
|
||||
let actions: JSX.Element | undefined;
|
||||
let additionalContent: JSX.Element | undefined;
|
||||
|
||||
|
@ -68,6 +79,7 @@ export const NotificationFollow: React.FC<{
|
|||
timestamp={notification.latest_page_notification_at}
|
||||
count={notification.notifications_count}
|
||||
labelRenderer={labelRenderer}
|
||||
labelSeeMoreHref={`/@${username}/followers`}
|
||||
unread={unread}
|
||||
actions={actions}
|
||||
additionalContent={additionalContent}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
import type { IconProp } from 'flavours/glitch/components/icon';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import Status from 'flavours/glitch/containers/status_container';
|
||||
import { getStatusHidden } from 'flavours/glitch/selectors/filters';
|
||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||
|
||||
import { DisplayedName } from './displayed_name';
|
||||
|
@ -51,6 +52,12 @@ export const NotificationWithStatus: React.FC<{
|
|||
(state) => state.statuses.getIn([statusId, 'visibility']) === 'direct',
|
||||
);
|
||||
|
||||
const isFiltered = useAppSelector(
|
||||
(state) =>
|
||||
statusId &&
|
||||
getStatusHidden(state, { id: statusId, contextType: 'notifications' }),
|
||||
);
|
||||
|
||||
const handlers = useMemo(
|
||||
() => ({
|
||||
open: () => {
|
||||
|
@ -77,7 +84,7 @@ export const NotificationWithStatus: React.FC<{
|
|||
[dispatch, statusId],
|
||||
);
|
||||
|
||||
if (!statusId) return null;
|
||||
if (!statusId || isFiltered) return null;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
|
|
|
@ -14,6 +14,8 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
|||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
|
||||
import StarIcon from '@/material-icons/400-24px/star.svg?react';
|
||||
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
|
||||
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
|
||||
import { replyCompose } from 'flavours/glitch/actions/compose';
|
||||
import { toggleReblog, toggleFavourite } from 'flavours/glitch/actions/interactions';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
|
@ -161,16 +163,20 @@ class Footer extends ImmutablePureComponent {
|
|||
replyTitle = intl.formatMessage(messages.replyAll);
|
||||
}
|
||||
|
||||
let reblogTitle = '';
|
||||
let reblogTitle, reblogIconComponent;
|
||||
|
||||
if (status.get('reblogged')) {
|
||||
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
|
||||
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
|
||||
} else if (publicStatus) {
|
||||
reblogTitle = intl.formatMessage(messages.reblog);
|
||||
reblogIconComponent = RepeatIcon;
|
||||
} else if (reblogPrivate) {
|
||||
reblogTitle = intl.formatMessage(messages.reblog_private);
|
||||
reblogIconComponent = RepeatPrivateIcon;
|
||||
} else {
|
||||
reblogTitle = intl.formatMessage(messages.cannot_reblog);
|
||||
reblogIconComponent = RepeatDisabledIcon;
|
||||
}
|
||||
|
||||
let replyButton = null;
|
||||
|
@ -201,7 +207,7 @@ class Footer extends ImmutablePureComponent {
|
|||
return (
|
||||
<div className='picture-in-picture__footer'>
|
||||
{replyButton}
|
||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={StarIcon} onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
|
||||
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' iconComponent={OpenInNewIcon} onClick={this.handleOpenClick} href={status.get('url')} />}
|
||||
</div>
|
||||
|
|
|
@ -116,6 +116,7 @@ export const MuteModal = ({ accountId, acct }) => {
|
|||
<div className='safety-action-modal__bottom__collapsible'>
|
||||
<div className='safety-action-modal__field-group'>
|
||||
<RadioButtonLabel name='duration' value='0' label={intl.formatMessage(messages.indefinite)} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||
<RadioButtonLabel name='duration' value='21600' label={intl.formatMessage(messages.hours, { number: 6 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||
<RadioButtonLabel name='duration' value='86400' label={intl.formatMessage(messages.hours, { number: 24 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||
<RadioButtonLabel name='duration' value='604800' label={intl.formatMessage(messages.days, { number: 7 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||
<RadioButtonLabel name='duration' value='2592000' label={intl.formatMessage(messages.days, { number: 30 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||
|
|
|
@ -159,7 +159,5 @@
|
|||
"status.local_only": "Only visible from your instance",
|
||||
"status.react": "React",
|
||||
"status.show_filter_reason": "Show anyway",
|
||||
"status.show_less": "Show less",
|
||||
"status.show_more": "Show more",
|
||||
"status.uncollapse": "Uncollapse"
|
||||
}
|
||||
|
|
|
@ -95,6 +95,9 @@ export const accountDefaultValues: AccountShape = {
|
|||
limited: false,
|
||||
moved: null,
|
||||
hide_collections: false,
|
||||
// This comes from `ApiMutedAccountJSON`, but we should eventually
|
||||
// store that in a different object.
|
||||
mute_expires_at: null,
|
||||
};
|
||||
|
||||
const AccountFactory = ImmutableRecord<AccountShape>(accountDefaultValues);
|
||||
|
|
|
@ -57,7 +57,10 @@ export const accountsReducer: Reducer<typeof initialState> = (
|
|||
return state.setIn([action.payload.id, 'hidden'], false);
|
||||
else if (importAccounts.match(action))
|
||||
return normalizeAccounts(state, action.payload.accounts);
|
||||
else if (followAccountSuccess.match(action)) {
|
||||
else if (
|
||||
followAccountSuccess.match(action) &&
|
||||
!action.payload.alreadyFollowing
|
||||
) {
|
||||
return state
|
||||
.update(action.payload.relationship.id, (account) =>
|
||||
account?.update('followers_count', (n) => n + 1),
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
unmountNotifications,
|
||||
refreshStaleNotificationGroups,
|
||||
pollRecentNotifications,
|
||||
shouldGroupNotificationType,
|
||||
} from 'flavours/glitch/actions/notification_groups';
|
||||
import {
|
||||
disconnectTimeline,
|
||||
|
@ -30,6 +29,7 @@ import {
|
|||
import type {
|
||||
ApiNotificationJSON,
|
||||
ApiNotificationGroupJSON,
|
||||
NotificationType,
|
||||
} from 'flavours/glitch/api_types/notifications';
|
||||
import { compareId } from 'flavours/glitch/compare_id';
|
||||
import { usePendingItems } from 'flavours/glitch/initial_state';
|
||||
|
@ -205,8 +205,9 @@ function mergeGapsAround(
|
|||
function processNewNotification(
|
||||
groups: NotificationGroupsState['groups'],
|
||||
notification: ApiNotificationJSON,
|
||||
groupedTypes: NotificationType[],
|
||||
) {
|
||||
if (!shouldGroupNotificationType(notification.type)) {
|
||||
if (!groupedTypes.includes(notification.type)) {
|
||||
notification = {
|
||||
...notification,
|
||||
group_key: `ungrouped-${notification.id}`,
|
||||
|
@ -476,11 +477,13 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
|||
trimNotifications(state);
|
||||
})
|
||||
.addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
|
||||
const notification = action.payload;
|
||||
if (notification) {
|
||||
if (action.payload) {
|
||||
const { notification, groupedTypes } = action.payload;
|
||||
|
||||
processNewNotification(
|
||||
usePendingItems ? state.pendingGroups : state.groups,
|
||||
notification,
|
||||
groupedTypes,
|
||||
);
|
||||
updateLastReadId(state);
|
||||
trimNotifications(state);
|
||||
|
@ -559,7 +562,10 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
|||
compareId(state.lastReadId, mostRecentGroup.page_max_id) < 0
|
||||
)
|
||||
state.lastReadId = mostRecentGroup.page_max_id;
|
||||
commitLastReadId(state);
|
||||
|
||||
// We don't call `commitLastReadId`, because that is conditional
|
||||
// and we want to unconditionally update the state instead.
|
||||
state.readMarkerId = state.lastReadId;
|
||||
})
|
||||
.addCase(fetchMarkers.fulfilled, (state, action) => {
|
||||
if (
|
||||
|
|
|
@ -82,6 +82,10 @@ const initialState = ImmutableMap({
|
|||
'admin.sign_up': true,
|
||||
'admin.report': true,
|
||||
}),
|
||||
|
||||
group: ImmutableMap({
|
||||
follow: true
|
||||
}),
|
||||
}),
|
||||
|
||||
firehose: ImmutableMap({
|
||||
|
|
50
app/javascript/flavours/glitch/selectors/filters.ts
Normal file
50
app/javascript/flavours/glitch/selectors/filters.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { createSelector } from '@reduxjs/toolkit';
|
||||
|
||||
import type { RootState } from 'flavours/glitch/store';
|
||||
import { toServerSideType } from 'flavours/glitch/utils/filters';
|
||||
|
||||
// TODO: move to `app/javascript/flavours/glitch/models` and use more globally
|
||||
type Filter = Immutable.Map<string, unknown>;
|
||||
|
||||
// TODO: move to `app/javascript/flavours/glitch/models` and use more globally
|
||||
type FilterResult = Immutable.Map<string, unknown>;
|
||||
|
||||
export const getFilters = createSelector(
|
||||
[
|
||||
(state: RootState) => state.filters as Immutable.Map<string, Filter>,
|
||||
(_, { contextType }: { contextType: string }) => contextType,
|
||||
],
|
||||
(filters, contextType) => {
|
||||
if (!contextType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const serverSideType = toServerSideType(contextType);
|
||||
|
||||
return filters.filter((filter) => {
|
||||
const context = filter.get('context') as Immutable.List<string>;
|
||||
const expiration = filter.get('expires_at') as Date | null;
|
||||
return (
|
||||
context.includes(serverSideType) &&
|
||||
(expiration === null || expiration > now)
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
export const getStatusHidden = (
|
||||
state: RootState,
|
||||
{ id, contextType }: { id: string; contextType: string },
|
||||
) => {
|
||||
const filters = getFilters(state, { contextType });
|
||||
if (filters === null) return false;
|
||||
|
||||
const filtered = state.statuses.getIn([id, 'filtered']) as
|
||||
| Immutable.List<FilterResult>
|
||||
| undefined;
|
||||
return filtered?.some(
|
||||
(result) =>
|
||||
filters.getIn([result.get('filter'), 'filter_action']) === 'hide',
|
||||
);
|
||||
};
|
|
@ -1,23 +1,12 @@
|
|||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { toServerSideType } from 'flavours/glitch/utils/filters';
|
||||
|
||||
import { me } from '../initial_state';
|
||||
|
||||
import { getFilters } from './filters';
|
||||
|
||||
export { makeGetAccount } from "./accounts";
|
||||
|
||||
const getFilters = createSelector([state => state.get('filters'), (_, { contextType }) => contextType], (filters, contextType) => {
|
||||
if (!contextType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const serverSideType = toServerSideType(contextType);
|
||||
|
||||
return filters.filter(filter => filter.get('context').includes(serverSideType) && (filter.get('expires_at') === null || filter.get('expires_at') > now));
|
||||
});
|
||||
|
||||
export const makeGetStatus = () => {
|
||||
return createSelector(
|
||||
[
|
||||
|
|
|
@ -52,4 +52,7 @@ export const selectSettingsNotificationsMinimizeFilteredBanner = (
|
|||
) =>
|
||||
state.settings.getIn(['notifications', 'minimizeFilteredBanner']) as boolean;
|
||||
|
||||
export const selectSettingsNotificationsGroupFollows = (state: RootState) =>
|
||||
state.settings.getIn(['notifications', 'group', 'follow']) as boolean;
|
||||
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
|
||||
|
|
|
@ -1050,6 +1050,12 @@ a.name-tag,
|
|||
color: var(--user-role-accent);
|
||||
}
|
||||
|
||||
.applications-list {
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.announcements-list,
|
||||
.filters-list {
|
||||
border: 1px solid var(--background-border-color);
|
||||
|
|
|
@ -1399,9 +1399,9 @@ body > [data-popper-placement] {
|
|||
}
|
||||
|
||||
.status__content__spoiler-link {
|
||||
display: inline-flex; // glitch: media icon in spoiler button
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
background: $action-button-color; // glitch: design used in more places
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: $inverted-text-color;
|
||||
font-weight: 700;
|
||||
|
@ -1411,23 +1411,6 @@ body > [data-popper-placement] {
|
|||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
vertical-align: top;
|
||||
align-items: center; // glitch: content indicator
|
||||
|
||||
&:hover {
|
||||
// glitch: design used in more places
|
||||
background: lighten($action-button-color, 7%);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.status__content__spoiler-icon {
|
||||
display: inline-block;
|
||||
margin-inline-start: 5px;
|
||||
border-inline-start: 1px solid currentColor;
|
||||
padding: 0;
|
||||
padding-inline-start: 4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.status__wrapper--filtered {
|
||||
|
@ -1952,6 +1935,14 @@ body > [data-popper-placement] {
|
|||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.content-warning {
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
@ -8542,79 +8533,23 @@ noscript {
|
|||
background: rgba($base-overlay-background, 0.5);
|
||||
}
|
||||
|
||||
.list-adder,
|
||||
.list-editor {
|
||||
background: $ui-base-color;
|
||||
backdrop-filter: var(--background-filter);
|
||||
background: var(--modal-background-color);
|
||||
border: 1px solid var(--modal-border-color);
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
||||
width: 380px;
|
||||
overflow: hidden;
|
||||
|
||||
@media screen and (width <= 420px) {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
h4 {
|
||||
padding: 15px 0;
|
||||
background: lighten($ui-base-color, 13%);
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.drawer__pager {
|
||||
height: 50vh;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.drawer__inner {
|
||||
border-radius: 0 0 8px 8px;
|
||||
|
||||
&.backdrop {
|
||||
width: calc(100% - 60px);
|
||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
||||
border-radius: 0 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__accounts {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.account__display-name {
|
||||
&:hover strong {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.account__avatar {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.list-adder {
|
||||
background: $ui-base-color;
|
||||
flex-direction: column;
|
||||
border-radius: 8px;
|
||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
||||
width: 380px;
|
||||
overflow: hidden;
|
||||
|
||||
@media screen and (width <= 420px) {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
&__account {
|
||||
background: lighten($ui-base-color, 13%);
|
||||
}
|
||||
|
||||
&__lists {
|
||||
background: lighten($ui-base-color, 13%);
|
||||
height: 50vh;
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow-y: auto;
|
||||
|
@ -8635,6 +8570,52 @@ noscript {
|
|||
text-decoration: none;
|
||||
font-size: 16px;
|
||||
padding: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.list-editor {
|
||||
h4 {
|
||||
padding: 15px 0;
|
||||
background: lighten($ui-base-color, 13%);
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.drawer__pager {
|
||||
height: 50vh;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.drawer__inner {
|
||||
&.backdrop {
|
||||
width: calc(100% - 60px);
|
||||
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
|
||||
border-radius: 0 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&__accounts {
|
||||
background: unset;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.account__display-name {
|
||||
&:hover strong {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.account__avatar {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11387,21 +11368,17 @@ noscript {
|
|||
color: $darker-text-color;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
max-height: 4 * 22px;
|
||||
max-height: none;
|
||||
overflow: hidden;
|
||||
|
||||
p {
|
||||
display: none;
|
||||
|
||||
&:first-child {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-indicator__attachments {
|
||||
|
@ -11686,19 +11663,21 @@ noscript {
|
|||
}
|
||||
|
||||
.content-warning {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
background: rgba($ui-highlight-color, 0.05);
|
||||
color: $secondary-text-color;
|
||||
border-top: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
border-color: rgba($ui-highlight-color, 0.15);
|
||||
border: 1px solid rgba($ui-highlight-color, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 8px (5px + 8px);
|
||||
position: relative;
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
cursor: pointer;
|
||||
|
||||
p {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.link-button {
|
||||
|
@ -11707,31 +11686,22 @@ noscript {
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
background: url('~images/warning-stripes.svg') repeat-y;
|
||||
width: 5px;
|
||||
top: 0;
|
||||
&--filter {
|
||||
color: $darker-text-color;
|
||||
|
||||
p {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
&::before {
|
||||
border-start-start-radius: 4px;
|
||||
border-end-start-radius: 4px;
|
||||
inset-inline-start: 0;
|
||||
.filter-name {
|
||||
font-weight: 500;
|
||||
color: $secondary-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
border-start-end-radius: 4px;
|
||||
border-end-end-radius: 4px;
|
||||
inset-inline-end: 0;
|
||||
}
|
||||
|
||||
&--filter::before,
|
||||
&--filter::after {
|
||||
background-image: url('~images/filter-stripes.svg');
|
||||
.status__content__spoiler-icon {
|
||||
float: inline-end;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,8 @@ code {
|
|||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 160px;
|
||||
max-width: 566px;
|
||||
margin-inline: auto;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
|
|
|
@ -76,4 +76,7 @@ body {
|
|||
--background-color-tint: rgba(255, 255, 255, 80%);
|
||||
--background-filter: blur(10px);
|
||||
--on-surface-color: #{transparentize($ui-base-color, 0.65)};
|
||||
--rich-text-container-color: rgba(255, 216, 231, 100%);
|
||||
--rich-text-text-color: rgba(114, 47, 83, 100%);
|
||||
--rich-text-decorations-color: rgba(255, 175, 212, 100%);
|
||||
}
|
||||
|
|
|
@ -2,9 +2,29 @@
|
|||
.e-content,
|
||||
.edit-indicator__content,
|
||||
.reply-indicator__content {
|
||||
code {
|
||||
background: var(--rich-text-container-color);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
color: var(--rich-text-text-color);
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--rich-text-container-color);
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
color: var(--rich-text-text-color);
|
||||
|
||||
code {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
pre,
|
||||
blockquote {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 22px;
|
||||
white-space: pre-wrap;
|
||||
unicode-bidi: plaintext;
|
||||
|
||||
|
@ -14,19 +34,45 @@
|
|||
}
|
||||
|
||||
blockquote {
|
||||
padding-inline-start: 10px;
|
||||
border-inline-start: 3px solid $darker-text-color;
|
||||
color: $darker-text-color;
|
||||
padding-inline-start: 32px;
|
||||
color: var(--rich-text-text-color);
|
||||
white-space: normal;
|
||||
position: relative;
|
||||
|
||||
p:last-child {
|
||||
&::before {
|
||||
display: block;
|
||||
content: '';
|
||||
width: 24px;
|
||||
height: 20px;
|
||||
mask-image: url('~images/quote.svg');
|
||||
background-color: var(--rich-text-decorations-color);
|
||||
position: absolute;
|
||||
inset-inline-start: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-top: 4px;
|
||||
border-inline-start: 3px solid var(--rich-text-decorations-color);
|
||||
padding-inline-start: 16px;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& > ul,
|
||||
& > ol {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 22px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
|
@ -76,7 +122,15 @@
|
|||
|
||||
ul,
|
||||
ol {
|
||||
margin-inline-start: 2em;
|
||||
padding-inline-start: 24px;
|
||||
|
||||
li {
|
||||
padding-inline-start: 8px;
|
||||
|
||||
&::marker {
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
@ -84,7 +138,11 @@
|
|||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
list-style-type: '•';
|
||||
|
||||
li::marker {
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
|
||||
ol {
|
||||
|
|
|
@ -90,6 +90,10 @@ body.rtl {
|
|||
direction: rtl;
|
||||
}
|
||||
|
||||
.column-back-button__icon {
|
||||
transform: scale(-1, 1);
|
||||
}
|
||||
|
||||
.simple_form select {
|
||||
background: $ui-base-color
|
||||
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 12%))}'/></svg>")
|
||||
|
|
|
@ -122,4 +122,7 @@ $dismiss-overlay-width: 4rem;
|
|||
--error-background-color: #{darken($error-red, 16%)};
|
||||
--error-active-background-color: #{darken($error-red, 12%)};
|
||||
--on-error-color: #fff;
|
||||
--rich-text-container-color: rgba(87, 24, 60, 100%);
|
||||
--rich-text-text-color: rgba(255, 175, 212, 100%);
|
||||
--rich-text-decorations-color: rgba(128, 58, 95, 100%);
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 5.6 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg"><symbol id="mastodon-svg-logo" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" /></symbol></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
3
app/javascript/images/quote.svg
Normal file
3
app/javascript/images/quote.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="20" viewBox="0 0 24 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M23.933 2.82414C22.324 4.07931 21.0726 5.3569 20.1788 6.6569C19.3296 7.91207 18.905 9.1 18.905 10.2207C19.0838 10.131 19.3073 10.0862 19.5754 10.0862C19.8883 10.0414 20.1564 10.019 20.3799 10.019C21.4078 10.019 22.257 10.4448 22.9274 11.2966C23.6425 12.1034 24 13.1121 24 14.3224C24 15.8017 23.5084 17.0345 22.5251 18.0207C21.5419 19.0069 20.3129 19.5 18.838 19.5C17.2737 19.5 16.0447 18.9397 15.1508 17.819C14.257 16.6535 13.8101 15.1069 13.8101 13.1793C13.8101 10.8931 14.5028 8.62931 15.8883 6.38793C17.2737 4.14655 19.3073 2.01724 21.9888 0L23.933 2.82414ZM10.1229 2.82414C8.51397 4.07931 7.26257 5.3569 6.36872 6.6569C5.51955 7.91207 5.09497 9.1 5.09497 10.2207C5.27374 10.131 5.49721 10.0862 5.76536 10.0862C6.07821 10.0414 6.34637 10.019 6.56983 10.019C7.59777 10.019 8.44693 10.4448 9.11732 11.2966C9.8324 12.1034 10.1899 13.1121 10.1899 14.3224C10.1899 15.8017 9.69832 17.0345 8.71508 18.0207C7.73184 19.0069 6.50279 19.5 5.02793 19.5C3.46369 19.5 2.23464 18.9397 1.34078 17.819C0.446927 16.6535 0 15.1069 0 13.1793C0 10.8931 0.692738 8.62931 2.07821 6.38793C3.46369 4.14655 5.49721 2.01724 8.17877 0L10.1229 2.82414Z" fill="currentColor" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -8,6 +8,7 @@ import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
|||
import type {
|
||||
ApiNotificationGroupJSON,
|
||||
ApiNotificationJSON,
|
||||
NotificationType,
|
||||
} from 'mastodon/api_types/notifications';
|
||||
import { allNotificationTypes } from 'mastodon/api_types/notifications';
|
||||
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
||||
|
@ -15,6 +16,7 @@ import { usePendingItems } from 'mastodon/initial_state';
|
|||
import type { NotificationGap } from 'mastodon/reducers/notification_groups';
|
||||
import {
|
||||
selectSettingsNotificationsExcludedTypes,
|
||||
selectSettingsNotificationsGroupFollows,
|
||||
selectSettingsNotificationsQuickFilterActive,
|
||||
selectSettingsNotificationsShows,
|
||||
} from 'mastodon/selectors/settings';
|
||||
|
@ -68,17 +70,19 @@ function dispatchAssociatedRecords(
|
|||
dispatch(importFetchedStatuses(fetchedStatuses));
|
||||
}
|
||||
|
||||
const supportedGroupedNotificationTypes = ['favourite', 'reblog'];
|
||||
function selectNotificationGroupedTypes(state: RootState) {
|
||||
const types: NotificationType[] = ['favourite', 'reblog'];
|
||||
|
||||
export function shouldGroupNotificationType(type: string) {
|
||||
return supportedGroupedNotificationTypes.includes(type);
|
||||
if (selectSettingsNotificationsGroupFollows(state)) types.push('follow');
|
||||
|
||||
return types;
|
||||
}
|
||||
|
||||
export const fetchNotifications = createDataLoadingThunk(
|
||||
'notificationGroups/fetch',
|
||||
async (_params, { getState }) =>
|
||||
apiFetchNotificationGroups({
|
||||
grouped_types: supportedGroupedNotificationTypes,
|
||||
grouped_types: selectNotificationGroupedTypes(getState()),
|
||||
exclude_types: getExcludedTypes(getState()),
|
||||
}),
|
||||
({ notifications, accounts, statuses }, { dispatch }) => {
|
||||
|
@ -102,7 +106,7 @@ export const fetchNotificationsGap = createDataLoadingThunk(
|
|||
'notificationGroups/fetchGap',
|
||||
async (params: { gap: NotificationGap }, { getState }) =>
|
||||
apiFetchNotificationGroups({
|
||||
grouped_types: supportedGroupedNotificationTypes,
|
||||
grouped_types: selectNotificationGroupedTypes(getState()),
|
||||
max_id: params.gap.maxId,
|
||||
exclude_types: getExcludedTypes(getState()),
|
||||
}),
|
||||
|
@ -119,7 +123,7 @@ export const pollRecentNotifications = createDataLoadingThunk(
|
|||
'notificationGroups/pollRecentNotifications',
|
||||
async (_params, { getState }) => {
|
||||
return apiFetchNotificationGroups({
|
||||
grouped_types: supportedGroupedNotificationTypes,
|
||||
grouped_types: selectNotificationGroupedTypes(getState()),
|
||||
max_id: undefined,
|
||||
exclude_types: getExcludedTypes(getState()),
|
||||
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones
|
||||
|
@ -168,7 +172,10 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
|
|||
|
||||
dispatchAssociatedRecords(dispatch, [notification]);
|
||||
|
||||
return notification;
|
||||
return {
|
||||
notification,
|
||||
groupedTypes: selectNotificationGroupedTypes(state),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ export interface ApiAccountRoleJSON {
|
|||
}
|
||||
|
||||
// See app/serializers/rest/account_serializer.rb
|
||||
export interface ApiAccountJSON {
|
||||
export interface BaseApiAccountJSON {
|
||||
acct: string;
|
||||
avatar: string;
|
||||
avatar_static: string;
|
||||
|
@ -45,3 +45,12 @@ export interface ApiAccountJSON {
|
|||
memorial?: boolean;
|
||||
hide_collections: boolean;
|
||||
}
|
||||
|
||||
// See app/serializers/rest/muted_account_serializer.rb
|
||||
export interface ApiMutedAccountJSON extends BaseApiAccountJSON {
|
||||
mute_expires_at?: string | null;
|
||||
}
|
||||
|
||||
// For now, we have the same type representing both `Account` and `MutedAccount`
|
||||
// objects, but we should refactor this in the future.
|
||||
export type ApiAccountJSON = ApiMutedAccountJSON;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import type { PropsWithChildren } from 'react';
|
||||
import type { PropsWithChildren, JSX } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
|
|
@ -8,7 +8,7 @@ export const ContentWarning: React.FC<{
|
|||
<StatusBanner
|
||||
expanded={expanded}
|
||||
onClick={onClick}
|
||||
variant={BannerVariant.Yellow}
|
||||
variant={BannerVariant.Warning}
|
||||
>
|
||||
<p dangerouslySetInnerHTML={{ __html: text }} />
|
||||
</StatusBanner>
|
||||
|
|
|
@ -10,13 +10,16 @@ export const FilterWarning: React.FC<{
|
|||
<StatusBanner
|
||||
expanded={expanded}
|
||||
onClick={onClick}
|
||||
variant={BannerVariant.Blue}
|
||||
variant={BannerVariant.Filter}
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='filter_warning.matches_filter'
|
||||
defaultMessage='Matches filter “{title}”'
|
||||
values={{ title }}
|
||||
defaultMessage='Matches filter “<span>{title}</span>”'
|
||||
values={{
|
||||
title,
|
||||
span: (chunks) => <span className='filter-name'>{chunks}</span>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</StatusBanner>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { memo } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||
|
||||
|
|
|
@ -449,7 +449,7 @@ class Status extends ImmutablePureComponent {
|
|||
} else if (status.get('media_attachments').size > 0) {
|
||||
const language = status.getIn(['translation', 'language']) || status.get('language');
|
||||
|
||||
if (['image', 'gifv'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
|
||||
if (['image', 'gifv', 'unknown'].includes(status.getIn(['media_attachments', 0, 'type'])) || status.get('media_attachments').size > 1) {
|
||||
media = (
|
||||
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
|
||||
{Component => (
|
||||
|
|
|
@ -264,7 +264,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
|
||||
}
|
||||
|
||||
if (publicStatus && (signedIn || !isRemote)) {
|
||||
if (publicStatus && !isRemote) {
|
||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
export enum BannerVariant {
|
||||
Yellow = 'yellow',
|
||||
Blue = 'blue',
|
||||
Warning = 'warning',
|
||||
Filter = 'filter',
|
||||
}
|
||||
|
||||
export const StatusBanner: React.FC<{
|
||||
|
@ -11,9 +11,9 @@ export const StatusBanner: React.FC<{
|
|||
expanded?: boolean;
|
||||
onClick?: () => void;
|
||||
}> = ({ children, variant, expanded, onClick }) => (
|
||||
<div
|
||||
<label
|
||||
className={
|
||||
variant === BannerVariant.Yellow
|
||||
variant === BannerVariant.Warning
|
||||
? 'content-warning'
|
||||
: 'content-warning content-warning--filter'
|
||||
}
|
||||
|
@ -26,6 +26,11 @@ export const StatusBanner: React.FC<{
|
|||
id='content_warning.hide'
|
||||
defaultMessage='Hide post'
|
||||
/>
|
||||
) : variant === BannerVariant.Warning ? (
|
||||
<FormattedMessage
|
||||
id='content_warning.show_more'
|
||||
defaultMessage='Show more'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='content_warning.show'
|
||||
|
@ -33,5 +38,5 @@ export const StatusBanner: React.FC<{
|
|||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
|
|
|
@ -21,9 +21,11 @@ class ColumnSettings extends PureComponent {
|
|||
|
||||
return (
|
||||
<div className='column-settings'>
|
||||
<section>
|
||||
<div className='column-settings__row'>
|
||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ const messages = defineMessages({
|
|||
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
||||
singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Pick one' },
|
||||
singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Single choice' },
|
||||
multipleChoice: { id: 'compose_form.poll.multiple', defaultMessage: 'Multiple choice' },
|
||||
});
|
||||
|
||||
|
|
|
@ -129,8 +129,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
|
||||
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
|
||||
setCanScrollRight(bodyRef.current.scrollLeft < 0);
|
||||
} else {
|
||||
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
||||
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
||||
}
|
||||
}, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
|
||||
|
||||
const handleLeftNav = useCallback(() => {
|
||||
|
@ -146,8 +151,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
|
||||
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
|
||||
setCanScrollRight(bodyRef.current.scrollLeft < 0);
|
||||
} else {
|
||||
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
||||
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
||||
}
|
||||
}, [setCanScrollRight, setCanScrollLeft, bodyRef]);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
|
|
|
@ -38,6 +38,7 @@ class ColumnSettings extends PureComponent {
|
|||
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
|
||||
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
|
||||
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />;
|
||||
const groupStr = <FormattedMessage id='notifications.column_settings.group' defaultMessage='Group' />;
|
||||
|
||||
const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
|
||||
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />;
|
||||
|
@ -94,6 +95,7 @@ class ColumnSettings extends PureComponent {
|
|||
{showPushSettings && <SettingToggle prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'follow']} onChange={this.onPushChange} label={pushStr} />}
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['shows', 'follow']} onChange={onChange} label={showStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
|
||||
<SettingToggle prefix='notifications' settings={settings} settingPath={['group', 'follow']} onChange={onChange} label={groupStr} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
@ -56,11 +56,12 @@ const mapDispatchToProps = (dispatch) => ({
|
|||
} else {
|
||||
dispatch(changeSetting(['notifications', ...path], checked));
|
||||
}
|
||||
} else if(path[0] === 'groupingBeta') {
|
||||
dispatch(changeSetting(['notifications', ...path], checked));
|
||||
dispatch(initializeNotifications());
|
||||
} else {
|
||||
dispatch(changeSetting(['notifications', ...path], checked));
|
||||
|
||||
if(path[0] === 'group' && path[1] === 'follow') {
|
||||
dispatch(initializeNotifications());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
import type { JSX } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
|
||||
import { FollowersCounter } from 'mastodon/components/counters';
|
||||
import { FollowButton } from 'mastodon/components/follow_button';
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import type { NotificationGroupFollow } from 'mastodon/models/notification_group';
|
||||
import { useAppSelector } from 'mastodon/store';
|
||||
|
||||
import type { LabelRenderer } from './notification_group_with_status';
|
||||
import { NotificationGroupWithStatus } from './notification_group_with_status';
|
||||
|
||||
const labelRenderer: LabelRenderer = (displayedName, total) => {
|
||||
const labelRenderer: LabelRenderer = (displayedName, total, seeMoreHref) => {
|
||||
if (total === 1)
|
||||
return (
|
||||
<FormattedMessage
|
||||
|
@ -23,10 +28,12 @@ const labelRenderer: LabelRenderer = (displayedName, total) => {
|
|||
return (
|
||||
<FormattedMessage
|
||||
id='notification.follow.name_and_others'
|
||||
defaultMessage='{name} and {count, plural, one {# other} other {# others}} followed you'
|
||||
defaultMessage='{name} and <a>{count, plural, one {# other} other {# others}}</a> followed you'
|
||||
values={{
|
||||
name: displayedName,
|
||||
count: total - 1,
|
||||
a: (chunks) =>
|
||||
seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -46,6 +53,10 @@ export const NotificationFollow: React.FC<{
|
|||
notification: NotificationGroupFollow;
|
||||
unread: boolean;
|
||||
}> = ({ notification, unread }) => {
|
||||
const username = useAppSelector(
|
||||
(state) => state.accounts.getIn([me, 'username']) as string,
|
||||
);
|
||||
|
||||
let actions: JSX.Element | undefined;
|
||||
let additionalContent: JSX.Element | undefined;
|
||||
|
||||
|
@ -68,6 +79,7 @@ export const NotificationFollow: React.FC<{
|
|||
timestamp={notification.latest_page_notification_at}
|
||||
count={notification.notifications_count}
|
||||
labelRenderer={labelRenderer}
|
||||
labelSeeMoreHref={`/@${username}/followers`}
|
||||
unread={unread}
|
||||
actions={actions}
|
||||
additionalContent={additionalContent}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useMemo } from 'react';
|
||||
import type { JSX } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
import type { IconProp } from 'mastodon/components/icon';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import Status from 'mastodon/containers/status_container';
|
||||
import { getStatusHidden } from 'mastodon/selectors/filters';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { DisplayedName } from './displayed_name';
|
||||
|
@ -48,6 +49,12 @@ export const NotificationWithStatus: React.FC<{
|
|||
(state) => state.statuses.getIn([statusId, 'visibility']) === 'direct',
|
||||
);
|
||||
|
||||
const isFiltered = useAppSelector(
|
||||
(state) =>
|
||||
statusId &&
|
||||
getStatusHidden(state, { id: statusId, contextType: 'notifications' }),
|
||||
);
|
||||
|
||||
const handlers = useMemo(
|
||||
() => ({
|
||||
open: () => {
|
||||
|
@ -73,7 +80,7 @@ export const NotificationWithStatus: React.FC<{
|
|||
[dispatch, statusId],
|
||||
);
|
||||
|
||||
if (!statusId) return null;
|
||||
if (!statusId || isFiltered) return null;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
|
|
|
@ -14,6 +14,8 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
|||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
|
||||
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
|
||||
import StarIcon from '@/material-icons/400-24px/star.svg?react';
|
||||
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
|
||||
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
|
||||
import { replyCompose } from 'mastodon/actions/compose';
|
||||
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
|
@ -159,22 +161,26 @@ class Footer extends ImmutablePureComponent {
|
|||
replyTitle = intl.formatMessage(messages.replyAll);
|
||||
}
|
||||
|
||||
let reblogTitle = '';
|
||||
let reblogTitle, reblogIconComponent;
|
||||
|
||||
if (status.get('reblogged')) {
|
||||
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
|
||||
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
|
||||
} else if (publicStatus) {
|
||||
reblogTitle = intl.formatMessage(messages.reblog);
|
||||
reblogIconComponent = RepeatIcon;
|
||||
} else if (reblogPrivate) {
|
||||
reblogTitle = intl.formatMessage(messages.reblog_private);
|
||||
reblogIconComponent = RepeatPrivateIcon;
|
||||
} else {
|
||||
reblogTitle = intl.formatMessage(messages.cannot_reblog);
|
||||
reblogIconComponent = RepeatDisabledIcon;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='picture-in-picture__footer'>
|
||||
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
|
||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={reblogIconComponent} onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={StarIcon} onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
|
||||
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' iconComponent={OpenInNewIcon} onClick={this.handleOpenClick} href={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`} />}
|
||||
</div>
|
||||
|
|
|
@ -116,6 +116,7 @@ export const MuteModal = ({ accountId, acct }) => {
|
|||
<div className='safety-action-modal__bottom__collapsible'>
|
||||
<div className='safety-action-modal__field-group'>
|
||||
<RadioButtonLabel name='duration' value='0' label={intl.formatMessage(messages.indefinite)} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||
<RadioButtonLabel name='duration' value='21600' label={intl.formatMessage(messages.hours, { number: 6 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||
<RadioButtonLabel name='duration' value='86400' label={intl.formatMessage(messages.hours, { number: 24 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||
<RadioButtonLabel name='duration' value='604800' label={intl.formatMessage(messages.days, { number: 7 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||
<RadioButtonLabel name='duration' value='2592000' label={intl.formatMessage(messages.days, { number: 30 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />
|
||||
|
|
|
@ -157,7 +157,6 @@
|
|||
"compose_form.poll.duration": "مُدَّة اِستطلاع الرأي",
|
||||
"compose_form.poll.multiple": "متعدد الخيارات",
|
||||
"compose_form.poll.option_placeholder": "الخيار {number}",
|
||||
"compose_form.poll.single": "اختر واحدا",
|
||||
"compose_form.poll.switch_to_multiple": "تغيِير الاستطلاع للسماح باِخيارات مُتعدِّدة",
|
||||
"compose_form.poll.switch_to_single": "تغيِير الاستطلاع للسماح باِخيار واحد فقط",
|
||||
"compose_form.poll.type": "الطراز",
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
"account.followers": "Siguidores",
|
||||
"account.followers.empty": "Naide sigue a esti perfil.",
|
||||
"account.follows.empty": "Esti perfil nun sigue a naide.",
|
||||
"account.hide_reblogs": "Anubrir los artículos compartíos de @{name}",
|
||||
"account.hide_reblogs": "Esconder los artículos compartíos de @{name}",
|
||||
"account.in_memoriam": "N'alcordanza.",
|
||||
"account.joined_short": "Data de xunión",
|
||||
"account.link_verified_on": "La propiedá d'esti enllaz comprobóse'l {date}",
|
||||
|
@ -122,6 +122,7 @@
|
|||
"conversation.open": "Ver la conversación",
|
||||
"conversation.with": "Con {names}",
|
||||
"copypaste.copied": "Copióse",
|
||||
"copypaste.copy_to_clipboard": "Copiar nel cartafueyu",
|
||||
"directory.federated": "Del fediversu conocíu",
|
||||
"directory.local": "De «{domain}» namás",
|
||||
"directory.new_arrivals": "Cuentes nueves",
|
||||
|
@ -130,7 +131,7 @@
|
|||
"dismissable_banner.dismiss": "Escartar",
|
||||
"dismissable_banner.explore_tags": "Esta seición contién les etiquetes del fediversu que tán ganando popularidá güei. Les etiquetes más usaes polos perfiles apaecen no cimero.",
|
||||
"dismissable_banner.public_timeline": "Esta seición contién los artículos más nuevos de les persones na web social que les persones de {domain} siguen.",
|
||||
"embed.instructions": "Empotra esti artículu nel to sitiu web pente la copia del códigu d'abaxo.",
|
||||
"embed.instructions": "Empotra esti artículu nel to sitiu web copiando'l códigu d'abaxo.",
|
||||
"embed.preview": "Va apaecer asina:",
|
||||
"emoji_button.activity": "Actividá",
|
||||
"emoji_button.flags": "Banderes",
|
||||
|
@ -251,7 +252,7 @@
|
|||
"keyboard_shortcuts.requests": "Abrir la llista de solicitúes de siguimientu",
|
||||
"keyboard_shortcuts.search": "Enfocar la barra de busca",
|
||||
"keyboard_shortcuts.start": "Abrir la columna «Entamar»",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "Amosar/anubrir el conteníu multimedia",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "Amosar/esconder el conteníu multimedia",
|
||||
"keyboard_shortcuts.toot": "Comenzar un artículu nuevu",
|
||||
"keyboard_shortcuts.unfocus": "Desenfocar l'área de composición/busca",
|
||||
"keyboard_shortcuts.up": "Xubir na llista",
|
||||
|
@ -299,6 +300,7 @@
|
|||
"notifications.column_settings.admin.sign_up": "Rexistros nuevos:",
|
||||
"notifications.column_settings.follow": "Siguidores nuevos:",
|
||||
"notifications.column_settings.follow_request": "Solicitúes de siguimientu nueves:",
|
||||
"notifications.column_settings.group": "Agrupar",
|
||||
"notifications.column_settings.mention": "Menciones:",
|
||||
"notifications.column_settings.poll": "Resultaos de les encuestes:",
|
||||
"notifications.column_settings.reblog": "Artículos compartíos:",
|
||||
|
@ -418,11 +420,12 @@
|
|||
"status.direct": "Mentar a @{name} per privao",
|
||||
"status.direct_indicator": "Mención privada",
|
||||
"status.edited_x_times": "Editóse {count, plural, one {{count} vegada} other {{count} vegaes}}",
|
||||
"status.embed": "Consiguir el códigu pa empotrar",
|
||||
"status.filter": "Peñerar esti artículu",
|
||||
"status.history.created": "{name} creó {date}",
|
||||
"status.history.edited": "{name} editó {date}",
|
||||
"status.load_more": "Cargar más",
|
||||
"status.media_hidden": "Conteníu multimedia anubríu",
|
||||
"status.media_hidden": "Conteníu multimedia escondíu",
|
||||
"status.mention": "Mentar a @{name}",
|
||||
"status.more": "Más",
|
||||
"status.mute": "Desactivar los avisos de @{name}",
|
||||
|
@ -468,14 +471,14 @@
|
|||
"upload_modal.applying": "Aplicando…",
|
||||
"upload_modal.detect_text": "Detectar el testu de la semeya",
|
||||
"upload_modal.edit_media": "Edición",
|
||||
"upload_modal.hint": "Calca o arrastra'l círculu de la previsualización pa escoyer el puntu d'enfoque que siempres va tar a la vista en toles miniatures.",
|
||||
"upload_modal.hint": "Calca o arrastra'l círculu de la previsualización pa escoyer el puntu d'enfoque que siempre va tar a la vista en toles miniatures.",
|
||||
"upload_progress.label": "Xubiendo…",
|
||||
"upload_progress.processing": "Procesando…",
|
||||
"video.close": "Zarrar el videu",
|
||||
"video.download": "Baxar el ficheru",
|
||||
"video.expand": "Espander el videu",
|
||||
"video.fullscreen": "Pantalla completa",
|
||||
"video.hide": "Anubrir el videu",
|
||||
"video.hide": "Esconder el videu",
|
||||
"video.mute": "Desactivar el soníu",
|
||||
"video.pause": "Posar",
|
||||
"video.play": "Reproducir",
|
||||
|
|
|
@ -156,7 +156,6 @@
|
|||
"compose_form.poll.duration": "Працягласць апытання",
|
||||
"compose_form.poll.multiple": "Множны выбар",
|
||||
"compose_form.poll.option_placeholder": "Варыянт {number}",
|
||||
"compose_form.poll.single": "Адзін варыянт",
|
||||
"compose_form.poll.switch_to_multiple": "Змяніце апытанне, каб дазволіць некалькі варыянтаў адказу",
|
||||
"compose_form.poll.switch_to_single": "Змяніце апытанне, каб дазволіць адзіны варыянт адказу",
|
||||
"compose_form.poll.type": "Стыль",
|
||||
|
|
|
@ -158,7 +158,6 @@
|
|||
"compose_form.poll.duration": "Времетраене на анкетата",
|
||||
"compose_form.poll.multiple": "Множествен избор",
|
||||
"compose_form.poll.option_placeholder": "Избор {number}",
|
||||
"compose_form.poll.single": "Подберете нещо",
|
||||
"compose_form.poll.switch_to_multiple": "Промяна на анкетата, за да се позволят множество възможни избора",
|
||||
"compose_form.poll.switch_to_single": "Промяна на анкетата, за да се позволи един възможен избор",
|
||||
"compose_form.poll.type": "Стил",
|
||||
|
@ -490,7 +489,6 @@
|
|||
"notification.favourite": "{name} направи любима публикацията ви",
|
||||
"notification.favourite.name_and_others_with_link": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> направиха любима ваша публикация",
|
||||
"notification.follow": "{name} ви последва",
|
||||
"notification.follow.name_and_others": "{name} и {count, plural, one {# друг} other {# други}} ви последваха",
|
||||
"notification.follow_request": "{name} поиска да ви последва",
|
||||
"notification.follow_request.name_and_others": "{name} и {count, plural, one {# друг} other {# други}} поискаха да ви последват",
|
||||
"notification.label.mention": "Споменаване",
|
||||
|
|
|
@ -145,7 +145,6 @@
|
|||
"compose_form.poll.duration": "Pad ar sontadeg",
|
||||
"compose_form.poll.multiple": "Meur a choaz",
|
||||
"compose_form.poll.option_placeholder": "Choaz {number}",
|
||||
"compose_form.poll.single": "Dibabit unan",
|
||||
"compose_form.poll.switch_to_multiple": "Kemmañ ar sontadeg evit aotren meur a zibab",
|
||||
"compose_form.poll.switch_to_single": "Kemmañ ar sontadeg evit aotren un dibab hepken",
|
||||
"compose_form.poll.type": "Neuz",
|
||||
|
@ -385,6 +384,7 @@
|
|||
"notification.admin.report": "Disklêriet eo bet {target} gant {name}",
|
||||
"notification.admin.sign_up": "{name} en·he deus lakaet e·hec'h anv",
|
||||
"notification.follow": "heuliañ a ra {name} ac'hanoc'h",
|
||||
"notification.follow.name_and_others": "{name} <a>{count, plural, one {hag # den all} two {ha # zen all} few {ha # den all} many {ha # den all} other {ha # den all}}</a> zo o heuliañ ac'hanoc'h",
|
||||
"notification.follow_request": "Gant {name} eo bet goulennet ho heuliañ",
|
||||
"notification.moderation-warning.learn_more": "Gouzout hiroc'h",
|
||||
"notification.own_poll": "Echu eo ho sontadeg",
|
||||
|
@ -399,6 +399,7 @@
|
|||
"notifications.column_settings.favourite": "Muiañ-karet:",
|
||||
"notifications.column_settings.follow": "Heulierien nevez:",
|
||||
"notifications.column_settings.follow_request": "Pedadoù heuliañ nevez :",
|
||||
"notifications.column_settings.group": "Strollañ",
|
||||
"notifications.column_settings.mention": "Menegoù:",
|
||||
"notifications.column_settings.poll": "Disoc'hoù ar sontadeg:",
|
||||
"notifications.column_settings.push": "Kemennoù push",
|
||||
|
|
|
@ -158,7 +158,6 @@
|
|||
"compose_form.poll.duration": "Durada de l'enquesta",
|
||||
"compose_form.poll.multiple": "Opcions múltiples",
|
||||
"compose_form.poll.option_placeholder": "Opció {number}",
|
||||
"compose_form.poll.single": "Trieu-ne una",
|
||||
"compose_form.poll.switch_to_multiple": "Canvia l’enquesta per a permetre múltiples opcions",
|
||||
"compose_form.poll.switch_to_single": "Canvia l’enquesta per a permetre una única opció",
|
||||
"compose_form.poll.type": "Estil",
|
||||
|
@ -508,7 +507,6 @@
|
|||
"notification.favourite": "{name} ha afavorit el teu tut",
|
||||
"notification.favourite.name_and_others_with_link": "{name} i <a>{count, plural, one {# altre} other {# altres}}</a> han afavorit la vostra publicació",
|
||||
"notification.follow": "{name} et segueix",
|
||||
"notification.follow.name_and_others": "{name} i {count, plural, one {# altre} other {# altres}} us han seguit",
|
||||
"notification.follow_request": "{name} ha sol·licitat de seguir-te",
|
||||
"notification.follow_request.name_and_others": "{name} i {count, plural, one {# altre} other {# altres}} han demanat de seguir-vos",
|
||||
"notification.label.mention": "Menció",
|
||||
|
|
|
@ -143,7 +143,6 @@
|
|||
"compose_form.poll.duration": "ماوەی ڕاپرسی",
|
||||
"compose_form.poll.multiple": "فرە هەڵبژاردە",
|
||||
"compose_form.poll.option_placeholder": "بژاردەی {number}",
|
||||
"compose_form.poll.single": "یەکێك هەلبژێرە",
|
||||
"compose_form.poll.switch_to_multiple": "ڕاپرسی بگۆڕە بۆ ڕێگەدان بە چەند هەڵبژاردنێک",
|
||||
"compose_form.poll.switch_to_single": "گۆڕینی ڕاپرسی بۆ ڕێگەدان بە تاکە هەڵبژاردنێک",
|
||||
"compose_form.poll.type": "ستایڵ",
|
||||
|
|
|
@ -157,7 +157,6 @@
|
|||
"compose_form.poll.duration": "Doba trvání ankety",
|
||||
"compose_form.poll.multiple": "Výběr z více možností",
|
||||
"compose_form.poll.option_placeholder": "Volba {number}",
|
||||
"compose_form.poll.single": "Vyber jednu",
|
||||
"compose_form.poll.switch_to_multiple": "Povolit u ankety výběr více voleb",
|
||||
"compose_form.poll.switch_to_single": "Povolit u ankety výběr pouze jedné volby",
|
||||
"compose_form.poll.type": "Styl",
|
||||
|
|
|
@ -158,7 +158,7 @@
|
|||
"compose_form.poll.duration": "Cyfnod pleidlais",
|
||||
"compose_form.poll.multiple": "Dewis lluosog",
|
||||
"compose_form.poll.option_placeholder": "Dewis {number}",
|
||||
"compose_form.poll.single": "Ddewis un",
|
||||
"compose_form.poll.single": "Dewis unigol",
|
||||
"compose_form.poll.switch_to_multiple": "Newid pleidlais i adael mwy nag un dewis",
|
||||
"compose_form.poll.switch_to_single": "Newid pleidlais i gyfyngu i un dewis",
|
||||
"compose_form.poll.type": "Arddull",
|
||||
|
@ -222,6 +222,7 @@
|
|||
"domain_block_modal.they_cant_follow": "Ni all neb o'r gweinydd hwn eich dilyn.",
|
||||
"domain_block_modal.they_wont_know": "Fyddan nhw ddim yn gwybod eu bod wedi cael eu blocio.",
|
||||
"domain_block_modal.title": "Blocio parth?",
|
||||
"domain_block_modal.you_will_lose_num_followers": "Byddwch yn colli {followersCount, plural, one {{followersCountDisplay} dilynwr} other {{followersCountDisplay} dilynwyr}} a {followingCount, plural, one {{followingCountDisplay} person rydych yn dilyn} other {{followingCountDisplay} o bobl rydych yn eu dilyn}}.",
|
||||
"domain_block_modal.you_will_lose_relationships": "Byddwch yn colli'r holl ddilynwyr a phobl rydych chi'n eu dilyn o'r gweinydd hwn.",
|
||||
"domain_block_modal.you_wont_see_posts": "Fyddwch chi ddim yn gweld postiadau na hysbysiadau gan ddefnyddwyr ar y gweinydd hwn.",
|
||||
"domain_pill.activitypub_lets_connect": "Mae'n caniatáu ichi gysylltu a rhyngweithio â phobl nid yn unig ar Mastodon, ond ar draws gwahanol apiau cymdeithasol hefyd.",
|
||||
|
@ -507,7 +508,7 @@
|
|||
"notification.favourite": "Ffafriodd {name} eich postiad",
|
||||
"notification.favourite.name_and_others_with_link": "Ffafriodd {name} a <a>{count, plural, one {# arall} other {# eraill}}</a> eich postiad",
|
||||
"notification.follow": "Dilynodd {name} chi",
|
||||
"notification.follow.name_and_others": "Mae {name} a {count, plural, one {# other} other {# others}} wedi'ch dilyn chi",
|
||||
"notification.follow.name_and_others": "Mae {name} a <a>{count, plural, zero {}one {# other} two {# others} few {# others} many {# others} other {# others}}</a> nawr yn eich dilyn chi",
|
||||
"notification.follow_request": "Mae {name} wedi gwneud cais i'ch dilyn",
|
||||
"notification.follow_request.name_and_others": "Mae {name} a{count, plural, one {# other} other {# others}} wedi gofyn i'ch dilyn chi",
|
||||
"notification.label.mention": "Crybwyll",
|
||||
|
@ -515,6 +516,7 @@
|
|||
"notification.label.private_reply": "Ateb preifat",
|
||||
"notification.label.reply": "Ateb",
|
||||
"notification.mention": "Crybwyll",
|
||||
"notification.mentioned_you": "Rydych wedi'ch crybwyll gan {name}",
|
||||
"notification.moderation-warning.learn_more": "Dysgu mwy",
|
||||
"notification.moderation_warning": "Rydych wedi derbyn rhybudd gan gymedrolwr",
|
||||
"notification.moderation_warning.action_delete_statuses": "Mae rhai o'ch postiadau wedi'u dileu.",
|
||||
|
@ -565,6 +567,7 @@
|
|||
"notifications.column_settings.filter_bar.category": "Bar hidlo cyflym",
|
||||
"notifications.column_settings.follow": "Dilynwyr newydd:",
|
||||
"notifications.column_settings.follow_request": "Ceisiadau dilyn newydd:",
|
||||
"notifications.column_settings.group": "Grŵp",
|
||||
"notifications.column_settings.mention": "Crybwylliadau:",
|
||||
"notifications.column_settings.poll": "Canlyniadau pleidlais:",
|
||||
"notifications.column_settings.push": "Hysbysiadau gwthiadwy",
|
||||
|
|
|
@ -158,7 +158,6 @@
|
|||
"compose_form.poll.duration": "Afstemningens varighed",
|
||||
"compose_form.poll.multiple": "Multivalg",
|
||||
"compose_form.poll.option_placeholder": "Valgmulighed {number}",
|
||||
"compose_form.poll.single": "Vælg én",
|
||||
"compose_form.poll.switch_to_multiple": "Ændr afstemning til flervalgstype",
|
||||
"compose_form.poll.switch_to_single": "Ændr afstemning til enkeltvalgstype",
|
||||
"compose_form.poll.type": "Stil",
|
||||
|
@ -508,7 +507,7 @@
|
|||
"notification.favourite": "{name} favoritmarkerede dit indlæg",
|
||||
"notification.favourite.name_and_others_with_link": "{name} og <a>{count, plural, one {# anden} other {# andre}}</a> gjorde dit indlæg til favorit",
|
||||
"notification.follow": "{name} begyndte at følge dig",
|
||||
"notification.follow.name_and_others": "{name} og {count, plural, one {# anden} other {# andre}} følger dig",
|
||||
"notification.follow.name_and_others": "{name} og <a>{count, plural, one {# andre} other {# andre}}</a> begyndte at følge dig",
|
||||
"notification.follow_request": "{name} har anmodet om at følge dig",
|
||||
"notification.follow_request.name_and_others": "{name} og {count, plural, one {# anden} other {# andre}} har anmodet om at følger dig",
|
||||
"notification.label.mention": "Omtale",
|
||||
|
@ -567,6 +566,7 @@
|
|||
"notifications.column_settings.filter_bar.category": "Hurtigfiltreringsbjælke",
|
||||
"notifications.column_settings.follow": "Nye følgere:",
|
||||
"notifications.column_settings.follow_request": "Nye følgeanmodninger:",
|
||||
"notifications.column_settings.group": "Gruppere",
|
||||
"notifications.column_settings.mention": "Omtaler:",
|
||||
"notifications.column_settings.poll": "Afstemningsresultater:",
|
||||
"notifications.column_settings.push": "Push-notifikationer",
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
"account.requested_follow": "{name} möchte dir folgen",
|
||||
"account.share": "Profil von @{name} teilen",
|
||||
"account.show_reblogs": "Geteilte Beiträge von @{name} anzeigen",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} posts}}",
|
||||
"account.statuses_counter": "{count, plural, one {{counter} Beitrag} other {{counter} Beiträge}}",
|
||||
"account.unblock": "Blockierung von @{name} aufheben",
|
||||
"account.unblock_domain": "Blockierung von {domain} aufheben",
|
||||
"account.unblock_short": "Blockierung aufheben",
|
||||
|
@ -508,7 +508,7 @@
|
|||
"notification.favourite": "{name} favorisierte deinen Beitrag",
|
||||
"notification.favourite.name_and_others_with_link": "{name} und <a>{count, plural, one {# weitere Person} other {# weitere Personen}}</a> favorisierten deinen Beitrag",
|
||||
"notification.follow": "{name} folgt dir",
|
||||
"notification.follow.name_and_others": "{name} und {count, plural, one {# weitere Person} other {# weitere Personen}} folgen dir",
|
||||
"notification.follow.name_and_others": "{name} und <a>{count, plural, one {# weitere Person} other {# weitere Personen}}</a> folgen dir",
|
||||
"notification.follow_request": "{name} möchte dir folgen",
|
||||
"notification.follow_request.name_and_others": "{name} und {count, plural, one {# weitere Person} other {# weitere Personen}} möchten dir folgen",
|
||||
"notification.label.mention": "Erwähnung",
|
||||
|
@ -567,6 +567,7 @@
|
|||
"notifications.column_settings.filter_bar.category": "Filterleiste",
|
||||
"notifications.column_settings.follow": "Neue Follower:",
|
||||
"notifications.column_settings.follow_request": "Neue Follower-Anfragen:",
|
||||
"notifications.column_settings.group": "Gruppieren",
|
||||
"notifications.column_settings.mention": "Erwähnungen:",
|
||||
"notifications.column_settings.poll": "Umfrageergebnisse:",
|
||||
"notifications.column_settings.push": "Push-Benachrichtigungen",
|
||||
|
|
|
@ -158,7 +158,6 @@
|
|||
"compose_form.poll.duration": "Διάρκεια δημοσκόπησης",
|
||||
"compose_form.poll.multiple": "Πολλαπλή επιλογή",
|
||||
"compose_form.poll.option_placeholder": "Επιλογή {number}",
|
||||
"compose_form.poll.single": "Διάλεξε ένα",
|
||||
"compose_form.poll.switch_to_multiple": "Ενημέρωση δημοσκόπησης με πολλαπλές επιλογές",
|
||||
"compose_form.poll.switch_to_single": "Ενημέρωση δημοσκόπησης με μοναδική επιλογή",
|
||||
"compose_form.poll.type": "Στυλ",
|
||||
|
@ -508,7 +507,6 @@
|
|||
"notification.favourite": "{name} favorited your post\n{name} προτίμησε την ανάρτηση σου",
|
||||
"notification.favourite.name_and_others_with_link": "{name} και <a>{count, plural, one {# ακόμη} other {# ακόμη}}</a> αγάπησαν την ανάρτησή σου",
|
||||
"notification.follow": "Ο/Η {name} σε ακολούθησε",
|
||||
"notification.follow.name_and_others": "{name} και {count, plural, one {# ακόμη} other {# ακόμη}} σε ακολούθησαν",
|
||||
"notification.follow_request": "Ο/H {name} ζήτησε να σε ακολουθήσει",
|
||||
"notification.follow_request.name_and_others": "{name} και {count, plural, one {# άλλος} other {# άλλοι}} ζήτησαν να σε ακολουθήσουν",
|
||||
"notification.label.mention": "Επισήμανση",
|
||||
|
|
|
@ -158,7 +158,6 @@
|
|||
"compose_form.poll.duration": "Poll duration",
|
||||
"compose_form.poll.multiple": "Multiple choice",
|
||||
"compose_form.poll.option_placeholder": "Option {number}",
|
||||
"compose_form.poll.single": "Pick one",
|
||||
"compose_form.poll.switch_to_multiple": "Change poll to allow multiple choices",
|
||||
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
|
||||
"compose_form.poll.type": "Style",
|
||||
|
@ -508,7 +507,6 @@
|
|||
"notification.favourite": "{name} favourited your post",
|
||||
"notification.favourite.name_and_others_with_link": "{name} and <a>{count, plural, one {# other} other {# others}}</a> favourited your post",
|
||||
"notification.follow": "{name} followed you",
|
||||
"notification.follow.name_and_others": "{name} and {count, plural, one {# other} other {# others}} followed you",
|
||||
"notification.follow_request": "{name} has requested to follow you",
|
||||
"notification.follow_request.name_and_others": "{name} and {count, plural, one {# other} other {# others}} has requested to follow you",
|
||||
"notification.label.mention": "Mention",
|
||||
|
@ -791,7 +789,7 @@
|
|||
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
|
||||
"status.embed": "Get embed code",
|
||||
"status.favourite": "Favourite",
|
||||
"status.favourites": "{count, plural, one {favorite} other {favorites}}",
|
||||
"status.favourites": "{count, plural, one {favourite} other {favourites}}",
|
||||
"status.filter": "Filter this post",
|
||||
"status.history.created": "{name} created {date}",
|
||||
"status.history.edited": "{name} edited {date}",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue