Merge branch 'upstream-main' into develop

This commit is contained in:
Jeremy Kescher 2024-11-01 17:13:04 +01:00
commit 7fa9c34dee
No known key found for this signature in database
GPG key ID: 80A419A7A613DFA4
433 changed files with 5909 additions and 3828 deletions

View file

@ -73,6 +73,16 @@ DB_PORT=5432
SECRET_KEY_BASE= SECRET_KEY_BASE=
OTP_SECRET= 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 # Web Push
# -------- # --------

View file

@ -1,6 +1,7 @@
name: Bug Report (Web Interface) name: Bug Report (Web Interface)
description: If you are using Mastodon's web interface and something is not working as expected description: There is a problem using Mastodon's web interface.
labels: [bug, 'status/to triage', 'area/web interface'] labels: ['status/to triage', 'area/web interface']
type: Bug
body: body:
- type: markdown - type: markdown
attributes: attributes:
@ -47,8 +48,8 @@ body:
attributes: attributes:
label: Mastodon version label: Mastodon version
description: | description: |
This is displayed at the bottom of the About page, eg. `v4.1.2+nightly-20230627` This is displayed at the bottom of the About page, eg. `v4.4.0-alpha.1`
placeholder: v4.1.2 placeholder: v4.3.0
validations: validations:
required: true required: true
- type: input - type: input
@ -56,7 +57,7 @@ body:
label: Browser name and version label: Browser name and version
description: | description: |
What browser are you using when getting this bug? Please specify the version as well. 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: validations:
required: true required: true
- type: input - type: input
@ -64,7 +65,7 @@ body:
label: Operating system label: Operating system
description: | description: |
What OS are you running? Please specify the version as well. What OS are you running? Please specify the version as well.
placeholder: macOS 13.4.1 placeholder: macOS 15.0.1
validations: validations:
required: true required: true
- type: textarea - type: textarea

View file

@ -1,7 +1,8 @@
name: Bug Report (server / API) name: Bug Report (server / API)
description: | description: |
If something is not working as expected, but is not from using the web interface. There is a problem with the HTTP server, REST API, ActivityPub interaction, etc.
labels: [bug, 'status/to triage'] labels: ['status/to triage']
type: 'Bug'
body: body:
- type: markdown - type: markdown
attributes: attributes:
@ -48,8 +49,8 @@ body:
attributes: attributes:
label: Mastodon version label: Mastodon version
description: | description: |
This is displayed at the bottom of the About page, eg. `v4.1.2+nightly-20230627` This is displayed at the bottom of the About page, eg. `v4.4.0-alpha.1`
placeholder: v4.1.2 placeholder: v4.3.0
validations: validations:
required: false required: false
- type: textarea - type: textarea
@ -59,7 +60,7 @@ body:
Any additional technical details you may have, like logs or error traces Any additional technical details you may have, like logs or error traces
value: | value: |
If this is happening on your own Mastodon server, please fill out those: If this is happening on your own Mastodon server, please fill out those:
- Ruby version: (from `ruby --version`, eg. v3.1.2) - Ruby version: (from `ruby --version`, eg. v3.3.5)
- Node.js version: (from `node --version`, eg. v18.16.0) - Node.js version: (from `node --version`, eg. v20.18.0)
validations: validations:
required: false required: false

View 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

View file

@ -1,6 +1,6 @@
name: Feature Request name: Feature Request
description: I have a suggestion description: I have a suggestion
labels: [suggestion] type: Suggestion
body: body:
- type: markdown - type: markdown
attributes: attributes:

View file

@ -21,9 +21,11 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- id: version_vars - id: version_vars
run: | 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: outputs:
metadata: ${{ steps.version_vars.outputs.mastodon_version_metadata }} metadata: ${{ steps.version_vars.outputs.mastodon_version_metadata }}
short_sha: ${{ steps.version_vars.outputs.mastodon_short_sha }}
build-image: build-image:
needs: compute-suffix needs: compute-suffix
@ -39,6 +41,7 @@ jobs:
latest=auto latest=auto
tags: | tags: |
type=ref,event=pr type=ref,event=pr
type=ref,event=pr,suffix=-${{ needs.compute-suffix.outputs.short_sha }}
secrets: inherit secrets: inherit
build-image-streaming: build-image-streaming:
@ -55,4 +58,5 @@ jobs:
latest=auto latest=auto
tags: | tags: |
type=ref,event=pr type=ref,event=pr
type=ref,event=pr,suffix=-${{ needs.compute-suffix.outputs.short_sha }}
secrets: inherit secrets: inherit

View file

@ -22,7 +22,7 @@ jobs:
# Only tag with latest when ran against the latest stable branch # Only tag with latest when ran against the latest stable branch
# This needs to be updated after each minor version release # This needs to be updated after each minor version release
flavor: | flavor: |
latest=${{ startsWith(github.ref, 'refs/tags/v4.2.') }} latest=${{ startsWith(github.ref, 'refs/tags/v4.3.') }}
tags: | tags: |
type=pep440,pattern={{raw}} type=pep440,pattern={{raw}}
type=pep440,pattern=v{{major}}.{{minor}} type=pep440,pattern=v{{major}}.{{minor}}

View file

@ -18,7 +18,7 @@ permissions:
jobs: jobs:
check-i18n: check-i18n:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

View file

@ -1,7 +1,6 @@
name: Crowdin / Upload translations name: Crowdin / Upload translations
on: on:
merge_group:
push: push:
branches: branches:
- 'main' - 'main'

View file

@ -32,6 +32,8 @@ jobs:
postgres: postgres:
- 14-alpine - 14-alpine
- 15-alpine - 15-alpine
- 16-alpine
- 17-alpine
services: services:
postgres: postgres:

View file

@ -143,7 +143,7 @@ jobs:
uses: ./.github/actions/setup-ruby uses: ./.github/actions/setup-ruby
with: with:
ruby-version: ${{ matrix.ruby-version}} ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg libpam-dev additional-system-dependencies: ffmpeg imagemagick libpam-dev
- name: Load database schema - name: Load database schema
run: | run: |
@ -245,7 +245,7 @@ jobs:
uses: ./.github/actions/setup-ruby uses: ./.github/actions/setup-ruby
with: with:
ruby-version: ${{ matrix.ruby-version}} ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg libpam-dev libyaml-dev additional-system-dependencies: ffmpeg libpam-dev
- name: Load database schema - name: Load database schema
run: './bin/rails db:create db:schema:load db:seed' run: './bin/rails db:create db:schema:load db:seed'
@ -325,7 +325,7 @@ jobs:
uses: ./.github/actions/setup-ruby uses: ./.github/actions/setup-ruby
with: with:
ruby-version: ${{ matrix.ruby-version}} ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg additional-system-dependencies: ffmpeg imagemagick
- name: Set up Javascript environment - name: Set up Javascript environment
uses: ./.github/actions/setup-javascript uses: ./.github/actions/setup-javascript
@ -445,7 +445,7 @@ jobs:
uses: ./.github/actions/setup-ruby uses: ./.github/actions/setup-ruby
with: with:
ruby-version: ${{ matrix.ruby-version}} ruby-version: ${{ matrix.ruby-version}}
additional-system-dependencies: ffmpeg additional-system-dependencies: ffmpeg imagemagick
- name: Set up Javascript environment - name: Set up Javascript environment
uses: ./.github/actions/setup-javascript uses: ./.github/actions/setup-javascript

View file

@ -67,7 +67,7 @@ The following changelog entries focus on changes visible to users, administrator
```html ```html
<meta name="fediverse:creator" content="username@domain" /> <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`.\ 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 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)\ - **Add in-app notifications for moderation actions and warnings** (#30065, #30082, and #30081 by @ClearlyClaire)\

View file

@ -191,7 +191,7 @@ FROM build AS libvips
# libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"] # libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"]
# renovate: datasource=github-releases depName=libvips packageName=libvips/libvips # 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"] # 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 ARG VIPS_URL=https://github.com/libvips/libvips/releases/download

View file

@ -61,7 +61,7 @@ gem 'irb', '~> 1.8'
gem 'kaminari', '~> 1.2' gem 'kaminari', '~> 1.2'
gem 'link_header', '~> 0.0' gem 'link_header', '~> 0.0'
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock' 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 'nokogiri', '~> 1.15'
gem 'oj', '~> 3.14' gem 'oj', '~> 3.14'
gem 'ox', '~> 2.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-http_client', '~> 0.22.3', require: false
gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false gem 'opentelemetry-instrumentation-net_http', '~> 0.22.4', require: false
gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false gem 'opentelemetry-instrumentation-pg', '~> 0.29.0', require: false
gem 'opentelemetry-instrumentation-rack', '~> 0.24.1', require: false gem 'opentelemetry-instrumentation-rack', '~> 0.25.0', require: false
gem 'opentelemetry-instrumentation-rails', '~> 0.31.0', require: false gem 'opentelemetry-instrumentation-rails', '~> 0.32.0', require: false
gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false gem 'opentelemetry-instrumentation-redis', '~> 0.25.3', require: false
gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false gem 'opentelemetry-instrumentation-sidekiq', '~> 0.25.2', require: false
gem 'opentelemetry-sdk', '~> 1.4', require: false gem 'opentelemetry-sdk', '~> 1.4', require: false

View file

@ -10,35 +10,35 @@ GIT
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
actioncable (7.1.4) actioncable (7.1.4.1)
actionpack (= 7.1.4) actionpack (= 7.1.4.1)
activesupport (= 7.1.4) activesupport (= 7.1.4.1)
nio4r (~> 2.0) nio4r (~> 2.0)
websocket-driver (>= 0.6.1) websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6) zeitwerk (~> 2.6)
actionmailbox (7.1.4) actionmailbox (7.1.4.1)
actionpack (= 7.1.4) actionpack (= 7.1.4.1)
activejob (= 7.1.4) activejob (= 7.1.4.1)
activerecord (= 7.1.4) activerecord (= 7.1.4.1)
activestorage (= 7.1.4) activestorage (= 7.1.4.1)
activesupport (= 7.1.4) activesupport (= 7.1.4.1)
mail (>= 2.7.1) mail (>= 2.7.1)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
actionmailer (7.1.4) actionmailer (7.1.4.1)
actionpack (= 7.1.4) actionpack (= 7.1.4.1)
actionview (= 7.1.4) actionview (= 7.1.4.1)
activejob (= 7.1.4) activejob (= 7.1.4.1)
activesupport (= 7.1.4) activesupport (= 7.1.4.1)
mail (~> 2.5, >= 2.5.4) mail (~> 2.5, >= 2.5.4)
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
actionpack (7.1.4) actionpack (7.1.4.1)
actionview (= 7.1.4) actionview (= 7.1.4.1)
activesupport (= 7.1.4) activesupport (= 7.1.4.1)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
racc racc
rack (>= 2.2.4) rack (>= 2.2.4)
@ -46,15 +46,15 @@ GEM
rack-test (>= 0.6.3) rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6) rails-html-sanitizer (~> 1.6)
actiontext (7.1.4) actiontext (7.1.4.1)
actionpack (= 7.1.4) actionpack (= 7.1.4.1)
activerecord (= 7.1.4) activerecord (= 7.1.4.1)
activestorage (= 7.1.4) activestorage (= 7.1.4.1)
activesupport (= 7.1.4) activesupport (= 7.1.4.1)
globalid (>= 0.6.0) globalid (>= 0.6.0)
nokogiri (>= 1.8.5) nokogiri (>= 1.8.5)
actionview (7.1.4) actionview (7.1.4.1)
activesupport (= 7.1.4) activesupport (= 7.1.4.1)
builder (~> 3.1) builder (~> 3.1)
erubi (~> 1.11) erubi (~> 1.11)
rails-dom-testing (~> 2.2) rails-dom-testing (~> 2.2)
@ -64,22 +64,22 @@ GEM
activemodel (>= 4.1) activemodel (>= 4.1)
case_transform (>= 0.2) case_transform (>= 0.2)
jsonapi-renderer (>= 0.1.1.beta1, < 0.3) jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
activejob (7.1.4) activejob (7.1.4.1)
activesupport (= 7.1.4) activesupport (= 7.1.4.1)
globalid (>= 0.3.6) globalid (>= 0.3.6)
activemodel (7.1.4) activemodel (7.1.4.1)
activesupport (= 7.1.4) activesupport (= 7.1.4.1)
activerecord (7.1.4) activerecord (7.1.4.1)
activemodel (= 7.1.4) activemodel (= 7.1.4.1)
activesupport (= 7.1.4) activesupport (= 7.1.4.1)
timeout (>= 0.4.0) timeout (>= 0.4.0)
activestorage (7.1.4) activestorage (7.1.4.1)
actionpack (= 7.1.4) actionpack (= 7.1.4.1)
activejob (= 7.1.4) activejob (= 7.1.4.1)
activerecord (= 7.1.4) activerecord (= 7.1.4.1)
activesupport (= 7.1.4) activesupport (= 7.1.4.1)
marcel (~> 1.0) marcel (~> 1.0)
activesupport (7.1.4) activesupport (7.1.4.1)
base64 base64
bigdecimal bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
@ -100,17 +100,17 @@ GEM
attr_required (1.0.2) attr_required (1.0.2)
awrence (1.2.1) awrence (1.2.1)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.983.0) aws-partitions (1.992.0)
aws-sdk-core (3.209.1) aws-sdk-core (3.210.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.94.0) aws-sdk-kms (1.95.0)
aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.167.0) aws-sdk-s3 (1.169.0)
aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.10.0) aws-sigv4 (1.10.0)
@ -137,7 +137,7 @@ GEM
blurhash (0.1.8) blurhash (0.1.8)
bootsnap (1.18.4) bootsnap (1.18.4)
msgpack (~> 1.2) msgpack (~> 1.2)
brakeman (6.2.1) brakeman (6.2.2)
racc racc
browser (5.3.1) browser (5.3.1)
brpoplpush-redis_script (0.1.3) brpoplpush-redis_script (0.1.3)
@ -233,7 +233,7 @@ GEM
tzinfo tzinfo
excon (0.111.0) excon (0.111.0)
fabrication (2.31.0) fabrication (2.31.0)
faker (3.4.2) faker (3.5.1)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (1.10.3) faraday (1.10.3)
faraday-em_http (~> 1.0) faraday-em_http (~> 1.0)
@ -429,9 +429,10 @@ GEM
azure-storage-blob (~> 2.0.1) azure-storage-blob (~> 2.0.1)
hashie (~> 5.0) hashie (~> 5.0)
memory_profiler (1.1.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.2015)
mime-types-data (3.2024.0820) mime-types-data (3.2024.1001)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.7) mini_portile2 (2.8.7)
minitest (5.25.1) minitest (5.25.1)
@ -503,7 +504,7 @@ GEM
opentelemetry-semantic_conventions opentelemetry-semantic_conventions
opentelemetry-helpers-sql-obfuscation (0.2.0) opentelemetry-helpers-sql-obfuscation (0.2.0)
opentelemetry-common (~> 0.21) opentelemetry-common (~> 0.21)
opentelemetry-instrumentation-action_mailer (0.1.0) opentelemetry-instrumentation-action_mailer (0.2.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.1) opentelemetry-instrumentation-active_support (~> 0.1)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
@ -515,13 +516,13 @@ GEM
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-active_support (~> 0.1) opentelemetry-instrumentation-active_support (~> 0.1)
opentelemetry-instrumentation-base (~> 0.22.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-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_model_serializers (0.20.2) opentelemetry-instrumentation-active_model_serializers (0.20.2)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) 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-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-active_support (0.6.0) opentelemetry-instrumentation-active_support (0.6.0)
@ -553,16 +554,16 @@ GEM
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-helpers-sql-obfuscation opentelemetry-helpers-sql-obfuscation
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rack (0.24.6) opentelemetry-instrumentation-rack (0.25.0)
opentelemetry-api (~> 1.0) opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-rails (0.31.2) opentelemetry-instrumentation-rails (0.32.0)
opentelemetry-api (~> 1.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_pack (~> 0.9.0)
opentelemetry-instrumentation-action_view (~> 0.7.0) opentelemetry-instrumentation-action_view (~> 0.7.0)
opentelemetry-instrumentation-active_job (~> 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-active_support (~> 0.6.0)
opentelemetry-instrumentation-base (~> 0.22.1) opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-redis (0.25.7) opentelemetry-instrumentation-redis (0.25.7)
@ -590,8 +591,8 @@ GEM
parslet (2.0.0) parslet (2.0.0)
pastel (0.8.0) pastel (0.8.0)
tty-color (~> 0.5) tty-color (~> 0.5)
pg (1.5.8) pg (1.5.9)
pghero (3.6.0) pghero (3.6.1)
activerecord (>= 6.1) activerecord (>= 6.1)
premailer (1.27.0) premailer (1.27.0)
addressable addressable
@ -615,7 +616,7 @@ GEM
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.8.1) racc (1.8.1)
rack (2.2.9) rack (2.2.10)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-cors (2.0.2) rack-cors (2.0.2)
@ -638,20 +639,20 @@ GEM
rackup (1.0.0) rackup (1.0.0)
rack (< 3) rack (< 3)
webrick webrick
rails (7.1.4) rails (7.1.4.1)
actioncable (= 7.1.4) actioncable (= 7.1.4.1)
actionmailbox (= 7.1.4) actionmailbox (= 7.1.4.1)
actionmailer (= 7.1.4) actionmailer (= 7.1.4.1)
actionpack (= 7.1.4) actionpack (= 7.1.4.1)
actiontext (= 7.1.4) actiontext (= 7.1.4.1)
actionview (= 7.1.4) actionview (= 7.1.4.1)
activejob (= 7.1.4) activejob (= 7.1.4.1)
activemodel (= 7.1.4) activemodel (= 7.1.4.1)
activerecord (= 7.1.4) activerecord (= 7.1.4.1)
activestorage (= 7.1.4) activestorage (= 7.1.4.1)
activesupport (= 7.1.4) activesupport (= 7.1.4.1)
bundler (>= 1.15.0) bundler (>= 1.15.0)
railties (= 7.1.4) railties (= 7.1.4.1)
rails-controller-testing (1.0.5) rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1) actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1)
@ -666,9 +667,9 @@ GEM
rails-i18n (7.0.9) rails-i18n (7.0.9)
i18n (>= 0.7, < 2) i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8) railties (>= 6.0.0, < 8)
railties (7.1.4) railties (7.1.4.1)
actionpack (= 7.1.4) actionpack (= 7.1.4.1)
activesupport (= 7.1.4) activesupport (= 7.1.4.1)
irb irb
rackup (>= 1.0.0) rackup (>= 1.0.0)
rake (>= 12.2) rake (>= 12.2)
@ -761,7 +762,7 @@ GEM
rubocop-rspec_rails (2.30.0) rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61) rubocop (~> 1.61)
rubocop-rspec (~> 3, >= 3.0.1) rubocop-rspec (~> 3, >= 3.0.1)
ruby-prof (1.7.0) ruby-prof (1.7.1)
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-saml (1.17.0) ruby-saml (1.17.0)
nokogiri (>= 1.13.10) nokogiri (>= 1.13.10)
@ -822,7 +823,7 @@ GEM
stoplight (4.1.0) stoplight (4.1.0)
redlock (~> 1.0) redlock (~> 1.0)
stringio (3.1.1) stringio (3.1.1)
strong_migrations (2.0.0) strong_migrations (2.0.1)
activerecord (>= 6.1) activerecord (>= 6.1)
swd (1.3.0) swd (1.3.0)
activesupport (>= 3) activesupport (>= 3)
@ -970,7 +971,7 @@ DEPENDENCIES
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
md-paperclip-azure (~> 2.2) md-paperclip-azure (~> 2.2)
memory_profiler memory_profiler
mime-types (~> 3.5.0) mime-types (~> 3.6.0)
net-http (~> 0.4.0) net-http (~> 0.4.0)
net-ldap (~> 0.18) net-ldap (~> 0.18)
nokogiri (~> 1.15) nokogiri (~> 1.15)
@ -991,8 +992,8 @@ DEPENDENCIES
opentelemetry-instrumentation-http_client (~> 0.22.3) opentelemetry-instrumentation-http_client (~> 0.22.3)
opentelemetry-instrumentation-net_http (~> 0.22.4) opentelemetry-instrumentation-net_http (~> 0.22.4)
opentelemetry-instrumentation-pg (~> 0.29.0) opentelemetry-instrumentation-pg (~> 0.29.0)
opentelemetry-instrumentation-rack (~> 0.24.1) opentelemetry-instrumentation-rack (~> 0.25.0)
opentelemetry-instrumentation-rails (~> 0.31.0) opentelemetry-instrumentation-rails (~> 0.32.0)
opentelemetry-instrumentation-redis (~> 0.25.3) opentelemetry-instrumentation-redis (~> 0.25.3)
opentelemetry-instrumentation-sidekiq (~> 0.25.2) opentelemetry-instrumentation-sidekiq (~> 0.25.2)
opentelemetry-sdk (~> 1.4) opentelemetry-sdk (~> 1.4)
@ -1057,7 +1058,7 @@ DEPENDENCIES
xorcist (~> 1.1) xorcist (~> 1.1)
RUBY VERSION RUBY VERSION
ruby 3.3.4p94 ruby 3.3.5p100
BUNDLED WITH BUNDLED WITH
2.5.18 2.5.22

View file

@ -52,7 +52,7 @@ class Api::V1::Notifications::RequestsController < Api::BaseController
private private
def load_requests 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), limit_param(DEFAULT_ACCOUNTS_LIMIT),
params_slice(:max_id, :since_id, :min_id) params_slice(:max_id, :since_id, :min_id)
) )

View file

@ -23,6 +23,6 @@ class Api::V1::Statuses::TranslationsController < Api::V1::Statuses::BaseControl
private private
def set_translation def set_translation
@translation = TranslateStatusService.new.call(@status, content_locale) @translation = TranslateStatusService.new.call(@status, I18n.locale.to_s)
end end
end end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class Api::Web::PushSubscriptionsController < Api::Web::BaseController 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 :set_push_subscription, only: :update
before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions? before_action :destroy_previous_subscriptions, only: :create, if: :prior_subscriptions?
after_action :update_session_with_subscription, only: :create 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 render json: @push_subscription, serializer: REST::WebPushSubscriptionSerializer
end end
def destroy
push_subscription = ::Web::PushSubscription.find_by_token_for(:unsubscribe, params[:id])
push_subscription&.destroy
head 200
end
private private
def active_session def active_session

View file

@ -35,7 +35,7 @@ class ApplicationController < ActionController::Base
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
rescue_from Mastodon::RateLimitExceededError, with: :too_many_requests 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 Mastodon::RaceConditionError, Stoplight::Error::RedLight, ActiveRecord::SerializationFailure, with: :service_unavailable
rescue_from Seahorse::Client::NetworkingError do |e| rescue_from Seahorse::Client::NetworkingError do |e|

View file

@ -20,7 +20,7 @@ module Api::ErrorHandling
render json: { error: 'Record not found' }, status: 404 render json: { error: 'Record not found' }, status: 404
end 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 render json: { error: 'Remote data could not be fetched' }, status: 503
end end

View file

@ -10,7 +10,7 @@ module Auth::CaptchaConcern
end end
def captcha_available? 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 end
def captcha_enabled? def captcha_enabled?

View file

@ -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'] 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 rescue SignatureVerificationError => e
fail_with! e.message 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}" fail_with! "Failed to fetch remote data: #{e.message}"
rescue Mastodon::UnexpectedResponseError rescue Mastodon::UnexpectedResponseError
fail_with! 'Failed to fetch remote data (got unexpected reply from server)' fail_with! 'Failed to fetch remote data (got unexpected reply from server)'

View file

@ -13,7 +13,7 @@ class MediaProxyController < ApplicationController
rescue_from ActiveRecord::RecordInvalid, with: :not_found rescue_from ActiveRecord::RecordInvalid, with: :not_found
rescue_from Mastodon::UnexpectedResponseError, with: :not_found rescue_from Mastodon::UnexpectedResponseError, with: :not_found
rescue_from Mastodon::NotPermittedError, 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 def show
with_redis_lock("media_download:#{params[:id]}") do with_redis_lock("media_download:#{params[:id]}") do

View file

@ -2,7 +2,7 @@
module Admin::SettingsHelper module Admin::SettingsHelper
def captcha_available? 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 end
def login_activity_title(activity) def login_activity_title(activity)

View file

@ -120,18 +120,6 @@ module ApplicationHelper
inline_svg_tag 'check.svg' inline_svg_tag 'check.svg'
end 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) def interrelationships_icon(relationships, account_id)
if relationships.following[account_id] && relationships.followed_by[account_id] if relationships.following[account_id] && relationships.followed_by[account_id]
material_symbol('sync_alt', title: I18n.t('relationships.mutual'), class: 'active passive') 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) tag.input(type: :text, maxlength: 999, spellcheck: false, readonly: true, **options)
end 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 # glitch-soc addition to handle the multiple flavors
def preload_locale_pack def preload_locale_pack
supported_locales = Themes.instance.flavour(current_flavour)['locales'] supported_locales = Themes.instance.flavour(current_flavour)['locales']

View file

@ -1,6 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
module FormattingHelper 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 = {}) def html_aware_format(text, local, options = {})
HtmlAwareFormatter.new(text, local, options).to_s HtmlAwareFormatter.new(text, local, options).to_s
end end
@ -23,33 +31,10 @@ module FormattingHelper
end end
def rss_status_content_format(status) 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( prerender_custom_emojis(
safe_join([before_html, html, after_html]), wrapped_status_content_format(status),
status.emojis, 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 ).to_str
end 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) html_aware_format(field.value, field.account.local?, with_rel_me: with_rel_me, with_domains: true, multiline: false)
end end
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 end

View file

@ -162,7 +162,7 @@ module LanguagesHelper
th: ['Thai', 'ไทย'].freeze, th: ['Thai', 'ไทย'].freeze,
ti: ['Tigrinya', 'ትግርኛ'].freeze, ti: ['Tigrinya', 'ትግርኛ'].freeze,
tk: ['Turkmen', 'Türkmen'].freeze, tk: ['Turkmen', 'Türkmen'].freeze,
tl: ['Tagalog', 'Wikang Tagalog'].freeze, tl: ['Tagalog', 'Tagalog'].freeze,
tn: ['Tswana', 'Setswana'].freeze, tn: ['Tswana', 'Setswana'].freeze,
to: ['Tonga', 'faka Tonga'].freeze, to: ['Tonga', 'faka Tonga'].freeze,
tr: ['Turkish', 'Türkçe'].freeze, tr: ['Turkish', 'Türkçe'].freeze,
@ -193,6 +193,7 @@ module LanguagesHelper
ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze, ckb: ['Sorani (Kurdish)', 'سۆرانی'].freeze,
cnr: ['Montenegrin', 'crnogorski'].freeze, cnr: ['Montenegrin', 'crnogorski'].freeze,
csb: ['Kashubian', 'Kaszëbsczi'].freeze, csb: ['Kashubian', 'Kaszëbsczi'].freeze,
gsw: ['Swiss German', 'Schwiizertütsch'].freeze,
jbo: ['Lojban', 'la .lojban.'].freeze, jbo: ['Lojban', 'la .lojban.'].freeze,
kab: ['Kabyle', 'Taqbaylit'].freeze, kab: ['Kabyle', 'Taqbaylit'].freeze,
ldn: ['Láadan', 'Láadan'].freeze, ldn: ['Láadan', 'Láadan'].freeze,

View file

@ -12,7 +12,7 @@ module StatusesHelper
}.freeze }.freeze
def nothing_here(extra_classes = '') 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') t('accounts.nothing_here')
end end
end end

View file

@ -327,31 +327,24 @@ Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
if (!input) return; if (!input) return;
const oldReadOnly = input.readOnly; navigator.clipboard
.writeText(input.value)
input.readOnly = false; .then(() => {
input.focus();
input.select();
input.setSelectionRange(0, input.value.length);
try {
if (document.execCommand('copy')) {
input.blur();
const parent = target.parentElement; const parent = target.parentElement;
if (!parent) return; if (parent) {
parent.classList.add('copied'); parent.classList.add('copied');
setTimeout(() => { setTimeout(() => {
parent.classList.remove('copied'); parent.classList.remove('copied');
}, 700); }, 700);
} }
} catch (err) {
console.error(err);
}
input.readOnly = oldReadOnly; return true;
})
.catch((error: unknown) => {
console.error(error);
});
}); });
const toggleSidebar = () => { const toggleSidebar = () => {

View file

@ -8,6 +8,7 @@ import type { ApiAccountJSON } from 'flavours/glitch/api_types/accounts';
import type { import type {
ApiNotificationGroupJSON, ApiNotificationGroupJSON,
ApiNotificationJSON, ApiNotificationJSON,
NotificationType,
} from 'flavours/glitch/api_types/notifications'; } from 'flavours/glitch/api_types/notifications';
import { allNotificationTypes } from 'flavours/glitch/api_types/notifications'; import { allNotificationTypes } from 'flavours/glitch/api_types/notifications';
import type { ApiStatusJSON } from 'flavours/glitch/api_types/statuses'; 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 type { NotificationGap } from 'flavours/glitch/reducers/notification_groups';
import { import {
selectSettingsNotificationsExcludedTypes, selectSettingsNotificationsExcludedTypes,
selectSettingsNotificationsGroupFollows,
selectSettingsNotificationsQuickFilterActive, selectSettingsNotificationsQuickFilterActive,
selectSettingsNotificationsShows, selectSettingsNotificationsShows,
} from 'flavours/glitch/selectors/settings'; } from 'flavours/glitch/selectors/settings';
@ -68,17 +70,19 @@ function dispatchAssociatedRecords(
dispatch(importFetchedStatuses(fetchedStatuses)); dispatch(importFetchedStatuses(fetchedStatuses));
} }
const supportedGroupedNotificationTypes = ['favourite', 'reblog']; function selectNotificationGroupedTypes(state: RootState) {
const types: NotificationType[] = ['favourite', 'reblog'];
export function shouldGroupNotificationType(type: string) { if (selectSettingsNotificationsGroupFollows(state)) types.push('follow');
return supportedGroupedNotificationTypes.includes(type);
return types;
} }
export const fetchNotifications = createDataLoadingThunk( export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch', 'notificationGroups/fetch',
async (_params, { getState }) => async (_params, { getState }) =>
apiFetchNotificationGroups({ apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes, grouped_types: selectNotificationGroupedTypes(getState()),
exclude_types: getExcludedTypes(getState()), exclude_types: getExcludedTypes(getState()),
}), }),
({ notifications, accounts, statuses }, { dispatch }) => { ({ notifications, accounts, statuses }, { dispatch }) => {
@ -102,7 +106,7 @@ export const fetchNotificationsGap = createDataLoadingThunk(
'notificationGroups/fetchGap', 'notificationGroups/fetchGap',
async (params: { gap: NotificationGap }, { getState }) => async (params: { gap: NotificationGap }, { getState }) =>
apiFetchNotificationGroups({ apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes, grouped_types: selectNotificationGroupedTypes(getState()),
max_id: params.gap.maxId, max_id: params.gap.maxId,
exclude_types: getExcludedTypes(getState()), exclude_types: getExcludedTypes(getState()),
}), }),
@ -119,7 +123,7 @@ export const pollRecentNotifications = createDataLoadingThunk(
'notificationGroups/pollRecentNotifications', 'notificationGroups/pollRecentNotifications',
async (_params, { getState }) => { async (_params, { getState }) => {
return apiFetchNotificationGroups({ return apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes, grouped_types: selectNotificationGroupedTypes(getState()),
max_id: undefined, max_id: undefined,
exclude_types: getExcludedTypes(getState()), exclude_types: getExcludedTypes(getState()),
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones // In slow mode, we don't want to include notifications that duplicate the already-displayed ones
@ -168,7 +172,10 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
dispatchAssociatedRecords(dispatch, [notification]); dispatchAssociatedRecords(dispatch, [notification]);
return notification; return {
notification,
groupedTypes: selectNotificationGroupedTypes(state),
};
}, },
); );

View file

@ -13,7 +13,7 @@ export interface ApiAccountRoleJSON {
} }
// See app/serializers/rest/account_serializer.rb // See app/serializers/rest/account_serializer.rb
export interface ApiAccountJSON { export interface BaseApiAccountJSON {
acct: string; acct: string;
avatar: string; avatar: string;
avatar_static: string; avatar_static: string;
@ -45,3 +45,12 @@ export interface ApiAccountJSON {
memorial?: boolean; memorial?: boolean;
hide_collections: 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;

View file

@ -1,4 +1,4 @@
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren, JSX } from 'react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';

View file

@ -1,6 +1,4 @@
/* Significantly rewritten from upstream to keep the old design for now */ import { StatusBanner, BannerVariant } from './status_banner';
import { FormattedMessage } from 'react-intl';
export const ContentWarning: React.FC<{ export const ContentWarning: React.FC<{
text: string; text: string;
@ -8,20 +6,12 @@ export const ContentWarning: React.FC<{
onClick?: () => void; onClick?: () => void;
icons?: React.ReactNode[]; icons?: React.ReactNode[];
}> = ({ text, expanded, onClick, icons }) => ( }> = ({ text, expanded, onClick, icons }) => (
<p> <StatusBanner
<span dangerouslySetInnerHTML={{ __html: text }} className='translate' />{' '} expanded={expanded}
<button
type='button'
className='status__content__spoiler-link'
onClick={onClick} 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} {icons}
</button> <p dangerouslySetInnerHTML={{ __html: text }} />
</p> </StatusBanner>
); );

View file

@ -10,13 +10,16 @@ export const FilterWarning: React.FC<{
<StatusBanner <StatusBanner
expanded={expanded} expanded={expanded}
onClick={onClick} onClick={onClick}
variant={BannerVariant.Blue} variant={BannerVariant.Filter}
> >
<p> <p>
<FormattedMessage <FormattedMessage
id='filter_warning.matches_filter' id='filter_warning.matches_filter'
defaultMessage='Matches filter “{title}”' defaultMessage='Matches filter “<span>{title}</span>”'
values={{ title }} values={{
title,
span: (chunks) => <span className='filter-name'>{chunks}</span>,
}}
/> />
</p> </p>
</StatusBanner> </StatusBanner>

View file

@ -1,4 +1,5 @@
import { memo } from 'react'; import { memo } from 'react';
import type { JSX } from 'react';
import { FormattedMessage, FormattedNumber } from 'react-intl'; import { FormattedMessage, FormattedNumber } from 'react-intl';

View file

@ -654,7 +654,7 @@ class Status extends ImmutablePureComponent {
media={status.get('media_attachments')} 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( media.push(
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}> <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => ( {Component => (

View file

@ -241,7 +241,7 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick }); 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 }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
} }

View file

@ -1,8 +1,8 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
export enum BannerVariant { export enum BannerVariant {
Yellow = 'yellow', Warning = 'warning',
Blue = 'blue', Filter = 'filter',
} }
export const StatusBanner: React.FC<{ export const StatusBanner: React.FC<{
@ -11,9 +11,9 @@ export const StatusBanner: React.FC<{
expanded?: boolean; expanded?: boolean;
onClick?: () => void; onClick?: () => void;
}> = ({ children, variant, expanded, onClick }) => ( }> = ({ children, variant, expanded, onClick }) => (
<div <label
className={ className={
variant === BannerVariant.Yellow variant === BannerVariant.Warning
? 'content-warning' ? 'content-warning'
: 'content-warning content-warning--filter' : 'content-warning content-warning--filter'
} }
@ -26,6 +26,11 @@ export const StatusBanner: React.FC<{
id='content_warning.hide' id='content_warning.hide'
defaultMessage='Hide post' defaultMessage='Hide post'
/> />
) : variant === BannerVariant.Warning ? (
<FormattedMessage
id='content_warning.show_more'
defaultMessage='Show more'
/>
) : ( ) : (
<FormattedMessage <FormattedMessage
id='content_warning.show' id='content_warning.show'
@ -33,5 +38,5 @@ export const StatusBanner: React.FC<{
/> />
)} )}
</button> </button>
</div> </label>
); );

View file

@ -378,7 +378,7 @@ class StatusContent extends PureComponent {
)).reduce((aggregate, item) => [...aggregate, item, ' '], []); )).reduce((aggregate, item) => [...aggregate, item, ' '], []);
let spoilerIcons = []; let spoilerIcons = [];
if (hidden && mediaIcons) { if (mediaIcons) {
const mediaComponents = { const mediaComponents = {
'link': LinkIcon, 'link': LinkIcon,
'picture-o': ImageIcon, 'picture-o': ImageIcon,

View file

@ -327,31 +327,24 @@ Rails.delegate(document, '.input-copy button', 'click', ({ target }) => {
if (!input) return; if (!input) return;
const oldReadOnly = input.readOnly; navigator.clipboard
.writeText(input.value)
input.readOnly = false; .then(() => {
input.focus();
input.select();
input.setSelectionRange(0, input.value.length);
try {
if (document.execCommand('copy')) {
input.blur();
const parent = target.parentElement; const parent = target.parentElement;
if (!parent) return; if (parent) {
parent.classList.add('copied'); parent.classList.add('copied');
setTimeout(() => { setTimeout(() => {
parent.classList.remove('copied'); parent.classList.remove('copied');
}, 700); }, 700);
} }
} catch (err) {
console.error(err);
}
input.readOnly = oldReadOnly; return true;
})
.catch((error: unknown) => {
console.error(error);
});
}); });
const toggleSidebar = () => { const toggleSidebar = () => {

View file

@ -27,15 +27,19 @@ class ColumnSettings extends PureComponent {
return ( return (
<div className='column-settings'> <div className='column-settings'>
<section>
<div className='column-settings__row'> <div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} /> <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
</div> </div>
</section>
<section>
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span> <span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
<div className='column-settings__row'> <div className='column-settings__row'>
<SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} /> <SettingText settings={settings} settingPath={['regex', 'body']} onChange={onChange} label={intl.formatMessage(messages.filter_regex)} />
</div> </div>
</section>
</div> </div>
); );
} }

View file

@ -26,7 +26,7 @@ const messages = defineMessages({
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Pick one' }, singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Single choice' },
multipleChoice: { id: 'compose_form.poll.multiple', defaultMessage: 'Multiple choice' }, multipleChoice: { id: 'compose_form.poll.multiple', defaultMessage: 'Multiple choice' },
}); });

View file

@ -129,8 +129,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
return; 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); setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth); setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
}
}, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]); }, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
const handleLeftNav = useCallback(() => { const handleLeftNav = useCallback(() => {
@ -146,8 +151,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
return; 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); setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth); setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
}
}, [setCanScrollRight, setCanScrollLeft, bodyRef]); }, [setCanScrollRight, setCanScrollLeft, bodyRef]);
const handleDismiss = useCallback(() => { const handleDismiss = useCallback(() => {

View file

@ -40,6 +40,7 @@ class ColumnSettings extends PureComponent {
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />; const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />; const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />; 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 showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />; 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={['shows', 'follow']} onChange={onChange} label={showStr} />
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} /> <PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} />
</div> </div>
<div className='column-settings__row'>
<SettingToggle prefix='notifications' settings={settings} settingPath={['group', 'follow']} onChange={onChange} label={groupStr} />
</div>
</section> </section>
<section role='group' aria-labelledby='notifications-follow-request'> <section role='group' aria-labelledby='notifications-follow-request'>

View file

@ -56,11 +56,12 @@ const mapDispatchToProps = (dispatch) => ({
} else { } else {
dispatch(changeSetting(['notifications', ...path], checked)); dispatch(changeSetting(['notifications', ...path], checked));
} }
} else if(path[0] === 'groupingBeta') {
dispatch(changeSetting(['notifications', ...path], checked));
dispatch(initializeNotifications());
} else { } else {
dispatch(changeSetting(['notifications', ...path], checked)); dispatch(changeSetting(['notifications', ...path], checked));
if(path[0] === 'group' && path[1] === 'follow') {
dispatch(initializeNotifications());
}
} }
}, },

View file

@ -1,16 +1,21 @@
import type { JSX } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react'; import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
import { FollowersCounter } from 'flavours/glitch/components/counters'; import { FollowersCounter } from 'flavours/glitch/components/counters';
import { FollowButton } from 'flavours/glitch/components/follow_button'; import { FollowButton } from 'flavours/glitch/components/follow_button';
import { ShortNumber } from 'flavours/glitch/components/short_number'; 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 type { NotificationGroupFollow } from 'flavours/glitch/models/notification_group';
import { useAppSelector } from 'flavours/glitch/store'; import { useAppSelector } from 'flavours/glitch/store';
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } 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) if (total === 1)
return ( return (
<FormattedMessage <FormattedMessage
@ -23,10 +28,12 @@ const labelRenderer: LabelRenderer = (displayedName, total) => {
return ( return (
<FormattedMessage <FormattedMessage
id='notification.follow.name_and_others' 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={{ values={{
name: displayedName, name: displayedName,
count: total - 1, count: total - 1,
a: (chunks) =>
seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks,
}} }}
/> />
); );
@ -46,6 +53,10 @@ export const NotificationFollow: React.FC<{
notification: NotificationGroupFollow; notification: NotificationGroupFollow;
unread: boolean; unread: boolean;
}> = ({ notification, unread }) => { }> = ({ notification, unread }) => {
const username = useAppSelector(
(state) => state.accounts.getIn([me, 'username']) as string,
);
let actions: JSX.Element | undefined; let actions: JSX.Element | undefined;
let additionalContent: JSX.Element | undefined; let additionalContent: JSX.Element | undefined;
@ -68,6 +79,7 @@ export const NotificationFollow: React.FC<{
timestamp={notification.latest_page_notification_at} timestamp={notification.latest_page_notification_at}
count={notification.notifications_count} count={notification.notifications_count}
labelRenderer={labelRenderer} labelRenderer={labelRenderer}
labelSeeMoreHref={`/@${username}/followers`}
unread={unread} unread={unread}
actions={actions} actions={actions}
additionalContent={additionalContent} additionalContent={additionalContent}

View file

@ -1,4 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { JSX } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';

View file

@ -16,6 +16,7 @@ import {
import type { IconProp } from 'flavours/glitch/components/icon'; import type { IconProp } from 'flavours/glitch/components/icon';
import { Icon } from 'flavours/glitch/components/icon'; import { Icon } from 'flavours/glitch/components/icon';
import Status from 'flavours/glitch/containers/status_container'; import Status from 'flavours/glitch/containers/status_container';
import { getStatusHidden } from 'flavours/glitch/selectors/filters';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store'; import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
import { DisplayedName } from './displayed_name'; import { DisplayedName } from './displayed_name';
@ -51,6 +52,12 @@ export const NotificationWithStatus: React.FC<{
(state) => state.statuses.getIn([statusId, 'visibility']) === 'direct', (state) => state.statuses.getIn([statusId, 'visibility']) === 'direct',
); );
const isFiltered = useAppSelector(
(state) =>
statusId &&
getStatusHidden(state, { id: statusId, contextType: 'notifications' }),
);
const handlers = useMemo( const handlers = useMemo(
() => ({ () => ({
open: () => { open: () => {
@ -77,7 +84,7 @@ export const NotificationWithStatus: React.FC<{
[dispatch, statusId], [dispatch, statusId],
); );
if (!statusId) return null; if (!statusId || isFiltered) return null;
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>

View file

@ -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 ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.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 { replyCompose } from 'flavours/glitch/actions/compose';
import { toggleReblog, toggleFavourite } from 'flavours/glitch/actions/interactions'; import { toggleReblog, toggleFavourite } from 'flavours/glitch/actions/interactions';
import { openModal } from 'flavours/glitch/actions/modal'; import { openModal } from 'flavours/glitch/actions/modal';
@ -161,16 +163,20 @@ class Footer extends ImmutablePureComponent {
replyTitle = intl.formatMessage(messages.replyAll); replyTitle = intl.formatMessage(messages.replyAll);
} }
let reblogTitle = ''; let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) { if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private); reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
} else if (publicStatus) { } else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog); reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) { } else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private); reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else { } else {
reblogTitle = intl.formatMessage(messages.cannot_reblog); reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
} }
let replyButton = null; let replyButton = null;
@ -201,7 +207,7 @@ class Footer extends ImmutablePureComponent {
return ( return (
<div className='picture-in-picture__footer'> <div className='picture-in-picture__footer'>
{replyButton} {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')} /> <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')} />} {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> </div>

View file

@ -116,6 +116,7 @@ export const MuteModal = ({ accountId, acct }) => {
<div className='safety-action-modal__bottom__collapsible'> <div className='safety-action-modal__bottom__collapsible'>
<div className='safety-action-modal__field-group'> <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='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='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='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} /> <RadioButtonLabel name='duration' value='2592000' label={intl.formatMessage(messages.days, { number: 30 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />

View file

@ -159,7 +159,5 @@
"status.local_only": "Only visible from your instance", "status.local_only": "Only visible from your instance",
"status.react": "React", "status.react": "React",
"status.show_filter_reason": "Show anyway", "status.show_filter_reason": "Show anyway",
"status.show_less": "Show less",
"status.show_more": "Show more",
"status.uncollapse": "Uncollapse" "status.uncollapse": "Uncollapse"
} }

View file

@ -95,6 +95,9 @@ export const accountDefaultValues: AccountShape = {
limited: false, limited: false,
moved: null, moved: null,
hide_collections: false, 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); const AccountFactory = ImmutableRecord<AccountShape>(accountDefaultValues);

View file

@ -57,7 +57,10 @@ export const accountsReducer: Reducer<typeof initialState> = (
return state.setIn([action.payload.id, 'hidden'], false); return state.setIn([action.payload.id, 'hidden'], false);
else if (importAccounts.match(action)) else if (importAccounts.match(action))
return normalizeAccounts(state, action.payload.accounts); return normalizeAccounts(state, action.payload.accounts);
else if (followAccountSuccess.match(action)) { else if (
followAccountSuccess.match(action) &&
!action.payload.alreadyFollowing
) {
return state return state
.update(action.payload.relationship.id, (account) => .update(action.payload.relationship.id, (account) =>
account?.update('followers_count', (n) => n + 1), account?.update('followers_count', (n) => n + 1),

View file

@ -21,7 +21,6 @@ import {
unmountNotifications, unmountNotifications,
refreshStaleNotificationGroups, refreshStaleNotificationGroups,
pollRecentNotifications, pollRecentNotifications,
shouldGroupNotificationType,
} from 'flavours/glitch/actions/notification_groups'; } from 'flavours/glitch/actions/notification_groups';
import { import {
disconnectTimeline, disconnectTimeline,
@ -30,6 +29,7 @@ import {
import type { import type {
ApiNotificationJSON, ApiNotificationJSON,
ApiNotificationGroupJSON, ApiNotificationGroupJSON,
NotificationType,
} from 'flavours/glitch/api_types/notifications'; } from 'flavours/glitch/api_types/notifications';
import { compareId } from 'flavours/glitch/compare_id'; import { compareId } from 'flavours/glitch/compare_id';
import { usePendingItems } from 'flavours/glitch/initial_state'; import { usePendingItems } from 'flavours/glitch/initial_state';
@ -205,8 +205,9 @@ function mergeGapsAround(
function processNewNotification( function processNewNotification(
groups: NotificationGroupsState['groups'], groups: NotificationGroupsState['groups'],
notification: ApiNotificationJSON, notification: ApiNotificationJSON,
groupedTypes: NotificationType[],
) { ) {
if (!shouldGroupNotificationType(notification.type)) { if (!groupedTypes.includes(notification.type)) {
notification = { notification = {
...notification, ...notification,
group_key: `ungrouped-${notification.id}`, group_key: `ungrouped-${notification.id}`,
@ -476,11 +477,13 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
trimNotifications(state); trimNotifications(state);
}) })
.addCase(processNewNotificationForGroups.fulfilled, (state, action) => { .addCase(processNewNotificationForGroups.fulfilled, (state, action) => {
const notification = action.payload; if (action.payload) {
if (notification) { const { notification, groupedTypes } = action.payload;
processNewNotification( processNewNotification(
usePendingItems ? state.pendingGroups : state.groups, usePendingItems ? state.pendingGroups : state.groups,
notification, notification,
groupedTypes,
); );
updateLastReadId(state); updateLastReadId(state);
trimNotifications(state); trimNotifications(state);
@ -559,7 +562,10 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
compareId(state.lastReadId, mostRecentGroup.page_max_id) < 0 compareId(state.lastReadId, mostRecentGroup.page_max_id) < 0
) )
state.lastReadId = mostRecentGroup.page_max_id; 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) => { .addCase(fetchMarkers.fulfilled, (state, action) => {
if ( if (

View file

@ -82,6 +82,10 @@ const initialState = ImmutableMap({
'admin.sign_up': true, 'admin.sign_up': true,
'admin.report': true, 'admin.report': true,
}), }),
group: ImmutableMap({
follow: true
}),
}), }),
firehose: ImmutableMap({ firehose: ImmutableMap({

View 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',
);
};

View file

@ -1,23 +1,12 @@
import { createSelector } from '@reduxjs/toolkit'; import { createSelector } from '@reduxjs/toolkit';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { toServerSideType } from 'flavours/glitch/utils/filters';
import { me } from '../initial_state'; import { me } from '../initial_state';
import { getFilters } from './filters';
export { makeGetAccount } from "./accounts"; 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 = () => { export const makeGetStatus = () => {
return createSelector( return createSelector(
[ [

View file

@ -52,4 +52,7 @@ export const selectSettingsNotificationsMinimizeFilteredBanner = (
) => ) =>
state.settings.getIn(['notifications', 'minimizeFilteredBanner']) as boolean; 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 */ /* eslint-enable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */

View file

@ -1050,6 +1050,12 @@ a.name-tag,
color: var(--user-role-accent); color: var(--user-role-accent);
} }
.applications-list {
.icon {
vertical-align: middle;
}
}
.announcements-list, .announcements-list,
.filters-list { .filters-list {
border: 1px solid var(--background-border-color); border: 1px solid var(--background-border-color);

View file

@ -1399,9 +1399,9 @@ body > [data-popper-placement] {
} }
.status__content__spoiler-link { .status__content__spoiler-link {
display: inline-flex; // glitch: media icon in spoiler button display: inline-block;
border-radius: 2px; border-radius: 2px;
background: $action-button-color; // glitch: design used in more places background: transparent;
border: 0; border: 0;
color: $inverted-text-color; color: $inverted-text-color;
font-weight: 700; font-weight: 700;
@ -1411,23 +1411,6 @@ body > [data-popper-placement] {
line-height: 20px; line-height: 20px;
cursor: pointer; cursor: pointer;
vertical-align: top; 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 { .status__wrapper--filtered {
@ -1952,6 +1935,14 @@ body > [data-popper-placement] {
margin-bottom: 16px; margin-bottom: 16px;
} }
.content-warning {
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.logo { .logo {
width: 40px; width: 40px;
height: 40px; height: 40px;
@ -8542,79 +8533,23 @@ noscript {
background: rgba($base-overlay-background, 0.5); background: rgba($base-overlay-background, 0.5);
} }
.list-adder,
.list-editor { .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; flex-direction: column;
border-radius: 8px; border-radius: 8px;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
width: 380px; width: 380px;
overflow: hidden; overflow: hidden;
@media screen and (width <= 420px) { @media screen and (width <= 420px) {
width: 90%; 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 { .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 { &__lists {
background: lighten($ui-base-color, 13%);
height: 50vh; height: 50vh;
border-radius: 0 0 8px 8px; border-radius: 0 0 8px 8px;
overflow-y: auto; overflow-y: auto;
@ -8635,6 +8570,52 @@ noscript {
text-decoration: none; text-decoration: none;
font-size: 16px; font-size: 16px;
padding: 10px; 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; color: $darker-text-color;
-webkit-line-clamp: 4; -webkit-line-clamp: 4;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
max-height: 4 * 22px; max-height: none;
overflow: hidden; overflow: hidden;
p {
display: none;
&:first-child {
display: initial;
}
}
p, p,
a { a {
color: inherit; color: inherit;
} }
p {
margin-bottom: 8px;
}
} }
.reply-indicator__attachments { .reply-indicator__attachments {
@ -11686,19 +11663,21 @@ noscript {
} }
.content-warning { .content-warning {
display: block;
box-sizing: border-box; box-sizing: border-box;
background: rgba($ui-highlight-color, 0.05); background: rgba($ui-highlight-color, 0.05);
color: $secondary-text-color; color: $secondary-text-color;
border-top: 1px solid; border: 1px solid rgba($ui-highlight-color, 0.15);
border-bottom: 1px solid; border-radius: 8px;
border-color: rgba($ui-highlight-color, 0.15);
padding: 8px (5px + 8px); padding: 8px (5px + 8px);
position: relative; position: relative;
font-size: 15px; font-size: 15px;
line-height: 22px; line-height: 22px;
cursor: pointer;
p { p {
margin-bottom: 8px; margin-bottom: 8px;
font-weight: 500;
} }
.link-button { .link-button {
@ -11707,31 +11686,22 @@ noscript {
font-weight: 500; font-weight: 500;
} }
&::before, &--filter {
&::after { color: $darker-text-color;
content: '';
display: block; p {
position: absolute; font-weight: normal;
height: 100%;
background: url('~images/warning-stripes.svg') repeat-y;
width: 5px;
top: 0;
} }
&::before { .filter-name {
border-start-start-radius: 4px; font-weight: 500;
border-end-start-radius: 4px; color: $secondary-text-color;
inset-inline-start: 0; }
} }
&::after { .status__content__spoiler-icon {
border-start-end-radius: 4px; float: inline-end;
border-end-end-radius: 4px; width: 20px;
inset-inline-end: 0; height: 20px;
}
&--filter::before,
&--filter::after {
background-image: url('~images/filter-stripes.svg');
} }
} }

View file

@ -23,6 +23,8 @@ code {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
height: 160px; height: 160px;
max-width: 566px;
margin-inline: auto;
&::after { &::after {
content: ''; content: '';

View file

@ -76,4 +76,7 @@ body {
--background-color-tint: rgba(255, 255, 255, 80%); --background-color-tint: rgba(255, 255, 255, 80%);
--background-filter: blur(10px); --background-filter: blur(10px);
--on-surface-color: #{transparentize($ui-base-color, 0.65)}; --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%);
} }

View file

@ -2,9 +2,29 @@
.e-content, .e-content,
.edit-indicator__content, .edit-indicator__content,
.reply-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, pre,
blockquote { blockquote {
margin-bottom: 20px; margin-bottom: 22px;
white-space: pre-wrap; white-space: pre-wrap;
unicode-bidi: plaintext; unicode-bidi: plaintext;
@ -14,19 +34,45 @@
} }
blockquote { blockquote {
padding-inline-start: 10px; padding-inline-start: 32px;
border-inline-start: 3px solid $darker-text-color; color: var(--rich-text-text-color);
color: $darker-text-color;
white-space: normal; 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; margin-bottom: 0;
} }
} }
& > ul, & > ul,
& > ol { & > ol {
margin-bottom: 20px; margin-bottom: 22px;
&:last-child {
margin-bottom: 0;
}
} }
h1, h1,
@ -76,7 +122,15 @@
ul, ul,
ol { ol {
margin-inline-start: 2em; padding-inline-start: 24px;
li {
padding-inline-start: 8px;
&::marker {
text-align: end;
}
}
p { p {
margin: 0; margin: 0;
@ -84,7 +138,11 @@
} }
ul { ul {
list-style-type: disc; list-style-type: '';
li::marker {
text-align: start;
}
} }
ol { ol {

View file

@ -90,6 +90,10 @@ body.rtl {
direction: rtl; direction: rtl;
} }
.column-back-button__icon {
transform: scale(-1, 1);
}
.simple_form select { .simple_form select {
background: $ui-base-color 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>") 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>")

View file

@ -122,4 +122,7 @@ $dismiss-overlay-width: 4rem;
--error-background-color: #{darken($error-red, 16%)}; --error-background-color: #{darken($error-red, 16%)};
--error-active-background-color: #{darken($error-red, 12%)}; --error-active-background-color: #{darken($error-red, 12%)};
--on-error-color: #fff; --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

View file

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

View 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

View file

@ -8,6 +8,7 @@ import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type { import type {
ApiNotificationGroupJSON, ApiNotificationGroupJSON,
ApiNotificationJSON, ApiNotificationJSON,
NotificationType,
} from 'mastodon/api_types/notifications'; } from 'mastodon/api_types/notifications';
import { allNotificationTypes } from 'mastodon/api_types/notifications'; import { allNotificationTypes } from 'mastodon/api_types/notifications';
import type { ApiStatusJSON } from 'mastodon/api_types/statuses'; 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 type { NotificationGap } from 'mastodon/reducers/notification_groups';
import { import {
selectSettingsNotificationsExcludedTypes, selectSettingsNotificationsExcludedTypes,
selectSettingsNotificationsGroupFollows,
selectSettingsNotificationsQuickFilterActive, selectSettingsNotificationsQuickFilterActive,
selectSettingsNotificationsShows, selectSettingsNotificationsShows,
} from 'mastodon/selectors/settings'; } from 'mastodon/selectors/settings';
@ -68,17 +70,19 @@ function dispatchAssociatedRecords(
dispatch(importFetchedStatuses(fetchedStatuses)); dispatch(importFetchedStatuses(fetchedStatuses));
} }
const supportedGroupedNotificationTypes = ['favourite', 'reblog']; function selectNotificationGroupedTypes(state: RootState) {
const types: NotificationType[] = ['favourite', 'reblog'];
export function shouldGroupNotificationType(type: string) { if (selectSettingsNotificationsGroupFollows(state)) types.push('follow');
return supportedGroupedNotificationTypes.includes(type);
return types;
} }
export const fetchNotifications = createDataLoadingThunk( export const fetchNotifications = createDataLoadingThunk(
'notificationGroups/fetch', 'notificationGroups/fetch',
async (_params, { getState }) => async (_params, { getState }) =>
apiFetchNotificationGroups({ apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes, grouped_types: selectNotificationGroupedTypes(getState()),
exclude_types: getExcludedTypes(getState()), exclude_types: getExcludedTypes(getState()),
}), }),
({ notifications, accounts, statuses }, { dispatch }) => { ({ notifications, accounts, statuses }, { dispatch }) => {
@ -102,7 +106,7 @@ export const fetchNotificationsGap = createDataLoadingThunk(
'notificationGroups/fetchGap', 'notificationGroups/fetchGap',
async (params: { gap: NotificationGap }, { getState }) => async (params: { gap: NotificationGap }, { getState }) =>
apiFetchNotificationGroups({ apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes, grouped_types: selectNotificationGroupedTypes(getState()),
max_id: params.gap.maxId, max_id: params.gap.maxId,
exclude_types: getExcludedTypes(getState()), exclude_types: getExcludedTypes(getState()),
}), }),
@ -119,7 +123,7 @@ export const pollRecentNotifications = createDataLoadingThunk(
'notificationGroups/pollRecentNotifications', 'notificationGroups/pollRecentNotifications',
async (_params, { getState }) => { async (_params, { getState }) => {
return apiFetchNotificationGroups({ return apiFetchNotificationGroups({
grouped_types: supportedGroupedNotificationTypes, grouped_types: selectNotificationGroupedTypes(getState()),
max_id: undefined, max_id: undefined,
exclude_types: getExcludedTypes(getState()), exclude_types: getExcludedTypes(getState()),
// In slow mode, we don't want to include notifications that duplicate the already-displayed ones // In slow mode, we don't want to include notifications that duplicate the already-displayed ones
@ -168,7 +172,10 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
dispatchAssociatedRecords(dispatch, [notification]); dispatchAssociatedRecords(dispatch, [notification]);
return notification; return {
notification,
groupedTypes: selectNotificationGroupedTypes(state),
};
}, },
); );

View file

@ -13,7 +13,7 @@ export interface ApiAccountRoleJSON {
} }
// See app/serializers/rest/account_serializer.rb // See app/serializers/rest/account_serializer.rb
export interface ApiAccountJSON { export interface BaseApiAccountJSON {
acct: string; acct: string;
avatar: string; avatar: string;
avatar_static: string; avatar_static: string;
@ -45,3 +45,12 @@ export interface ApiAccountJSON {
memorial?: boolean; memorial?: boolean;
hide_collections: 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;

View file

@ -1,4 +1,4 @@
import type { PropsWithChildren } from 'react'; import type { PropsWithChildren, JSX } from 'react';
import { useCallback } from 'react'; import { useCallback } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';

View file

@ -8,7 +8,7 @@ export const ContentWarning: React.FC<{
<StatusBanner <StatusBanner
expanded={expanded} expanded={expanded}
onClick={onClick} onClick={onClick}
variant={BannerVariant.Yellow} variant={BannerVariant.Warning}
> >
<p dangerouslySetInnerHTML={{ __html: text }} /> <p dangerouslySetInnerHTML={{ __html: text }} />
</StatusBanner> </StatusBanner>

View file

@ -10,13 +10,16 @@ export const FilterWarning: React.FC<{
<StatusBanner <StatusBanner
expanded={expanded} expanded={expanded}
onClick={onClick} onClick={onClick}
variant={BannerVariant.Blue} variant={BannerVariant.Filter}
> >
<p> <p>
<FormattedMessage <FormattedMessage
id='filter_warning.matches_filter' id='filter_warning.matches_filter'
defaultMessage='Matches filter “{title}”' defaultMessage='Matches filter “<span>{title}</span>”'
values={{ title }} values={{
title,
span: (chunks) => <span className='filter-name'>{chunks}</span>,
}}
/> />
</p> </p>
</StatusBanner> </StatusBanner>

View file

@ -1,4 +1,5 @@
import { memo } from 'react'; import { memo } from 'react';
import type { JSX } from 'react';
import { FormattedMessage, FormattedNumber } from 'react-intl'; import { FormattedMessage, FormattedNumber } from 'react-intl';

View file

@ -449,7 +449,7 @@ class Status extends ImmutablePureComponent {
} else if (status.get('media_attachments').size > 0) { } else if (status.get('media_attachments').size > 0) {
const language = status.getIn(['translation', 'language']) || status.get('language'); 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 = ( media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}> <Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => ( {Component => (

View file

@ -264,7 +264,7 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick }); 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 }); menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
} }

View file

@ -1,8 +1,8 @@
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
export enum BannerVariant { export enum BannerVariant {
Yellow = 'yellow', Warning = 'warning',
Blue = 'blue', Filter = 'filter',
} }
export const StatusBanner: React.FC<{ export const StatusBanner: React.FC<{
@ -11,9 +11,9 @@ export const StatusBanner: React.FC<{
expanded?: boolean; expanded?: boolean;
onClick?: () => void; onClick?: () => void;
}> = ({ children, variant, expanded, onClick }) => ( }> = ({ children, variant, expanded, onClick }) => (
<div <label
className={ className={
variant === BannerVariant.Yellow variant === BannerVariant.Warning
? 'content-warning' ? 'content-warning'
: 'content-warning content-warning--filter' : 'content-warning content-warning--filter'
} }
@ -26,6 +26,11 @@ export const StatusBanner: React.FC<{
id='content_warning.hide' id='content_warning.hide'
defaultMessage='Hide post' defaultMessage='Hide post'
/> />
) : variant === BannerVariant.Warning ? (
<FormattedMessage
id='content_warning.show_more'
defaultMessage='Show more'
/>
) : ( ) : (
<FormattedMessage <FormattedMessage
id='content_warning.show' id='content_warning.show'
@ -33,5 +38,5 @@ export const StatusBanner: React.FC<{
/> />
)} )}
</button> </button>
</div> </label>
); );

View file

@ -21,9 +21,11 @@ class ColumnSettings extends PureComponent {
return ( return (
<div className='column-settings'> <div className='column-settings'>
<section>
<div className='column-settings__row'> <div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} /> <SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
</div> </div>
</section>
</div> </div>
); );
} }

View file

@ -25,7 +25,7 @@ const messages = defineMessages({
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Pick one' }, singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Single choice' },
multipleChoice: { id: 'compose_form.poll.multiple', defaultMessage: 'Multiple choice' }, multipleChoice: { id: 'compose_form.poll.multiple', defaultMessage: 'Multiple choice' },
}); });

View file

@ -129,8 +129,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
return; 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); setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth); setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
}
}, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]); }, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
const handleLeftNav = useCallback(() => { const handleLeftNav = useCallback(() => {
@ -146,8 +151,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
return; 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); setCanScrollLeft(bodyRef.current.scrollLeft > 0);
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth); setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
}
}, [setCanScrollRight, setCanScrollLeft, bodyRef]); }, [setCanScrollRight, setCanScrollLeft, bodyRef]);
const handleDismiss = useCallback(() => { const handleDismiss = useCallback(() => {

View file

@ -38,6 +38,7 @@ class ColumnSettings extends PureComponent {
const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />; const alertStr = <FormattedMessage id='notifications.column_settings.alert' defaultMessage='Desktop notifications' />;
const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />; const showStr = <FormattedMessage id='notifications.column_settings.show' defaultMessage='Show in column' />;
const soundStr = <FormattedMessage id='notifications.column_settings.sound' defaultMessage='Play sound' />; 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 showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed');
const pushStr = showPushSettings && <FormattedMessage id='notifications.column_settings.push' defaultMessage='Push notifications' />; 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} />} {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={['shows', 'follow']} onChange={onChange} label={showStr} />
<SettingToggle prefix='notifications' settings={settings} settingPath={['sounds', 'follow']} onChange={onChange} label={soundStr} /> <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> </div>
</section> </section>

View file

@ -56,11 +56,12 @@ const mapDispatchToProps = (dispatch) => ({
} else { } else {
dispatch(changeSetting(['notifications', ...path], checked)); dispatch(changeSetting(['notifications', ...path], checked));
} }
} else if(path[0] === 'groupingBeta') {
dispatch(changeSetting(['notifications', ...path], checked));
dispatch(initializeNotifications());
} else { } else {
dispatch(changeSetting(['notifications', ...path], checked)); dispatch(changeSetting(['notifications', ...path], checked));
if(path[0] === 'group' && path[1] === 'follow') {
dispatch(initializeNotifications());
}
} }
}, },

View file

@ -1,16 +1,21 @@
import type { JSX } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react'; import PersonAddIcon from '@/material-icons/400-24px/person_add-fill.svg?react';
import { FollowersCounter } from 'mastodon/components/counters'; import { FollowersCounter } from 'mastodon/components/counters';
import { FollowButton } from 'mastodon/components/follow_button'; import { FollowButton } from 'mastodon/components/follow_button';
import { ShortNumber } from 'mastodon/components/short_number'; import { ShortNumber } from 'mastodon/components/short_number';
import { me } from 'mastodon/initial_state';
import type { NotificationGroupFollow } from 'mastodon/models/notification_group'; import type { NotificationGroupFollow } from 'mastodon/models/notification_group';
import { useAppSelector } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store';
import type { LabelRenderer } from './notification_group_with_status'; import type { LabelRenderer } from './notification_group_with_status';
import { NotificationGroupWithStatus } 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) if (total === 1)
return ( return (
<FormattedMessage <FormattedMessage
@ -23,10 +28,12 @@ const labelRenderer: LabelRenderer = (displayedName, total) => {
return ( return (
<FormattedMessage <FormattedMessage
id='notification.follow.name_and_others' 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={{ values={{
name: displayedName, name: displayedName,
count: total - 1, count: total - 1,
a: (chunks) =>
seeMoreHref ? <Link to={seeMoreHref}>{chunks}</Link> : chunks,
}} }}
/> />
); );
@ -46,6 +53,10 @@ export const NotificationFollow: React.FC<{
notification: NotificationGroupFollow; notification: NotificationGroupFollow;
unread: boolean; unread: boolean;
}> = ({ notification, unread }) => { }> = ({ notification, unread }) => {
const username = useAppSelector(
(state) => state.accounts.getIn([me, 'username']) as string,
);
let actions: JSX.Element | undefined; let actions: JSX.Element | undefined;
let additionalContent: JSX.Element | undefined; let additionalContent: JSX.Element | undefined;
@ -68,6 +79,7 @@ export const NotificationFollow: React.FC<{
timestamp={notification.latest_page_notification_at} timestamp={notification.latest_page_notification_at}
count={notification.notifications_count} count={notification.notifications_count}
labelRenderer={labelRenderer} labelRenderer={labelRenderer}
labelSeeMoreHref={`/@${username}/followers`}
unread={unread} unread={unread}
actions={actions} actions={actions}
additionalContent={additionalContent} additionalContent={additionalContent}

View file

@ -1,4 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import type { JSX } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';

View file

@ -13,6 +13,7 @@ import {
import type { IconProp } from 'mastodon/components/icon'; import type { IconProp } from 'mastodon/components/icon';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import Status from 'mastodon/containers/status_container'; import Status from 'mastodon/containers/status_container';
import { getStatusHidden } from 'mastodon/selectors/filters';
import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { DisplayedName } from './displayed_name'; import { DisplayedName } from './displayed_name';
@ -48,6 +49,12 @@ export const NotificationWithStatus: React.FC<{
(state) => state.statuses.getIn([statusId, 'visibility']) === 'direct', (state) => state.statuses.getIn([statusId, 'visibility']) === 'direct',
); );
const isFiltered = useAppSelector(
(state) =>
statusId &&
getStatusHidden(state, { id: statusId, contextType: 'notifications' }),
);
const handlers = useMemo( const handlers = useMemo(
() => ({ () => ({
open: () => { open: () => {
@ -73,7 +80,7 @@ export const NotificationWithStatus: React.FC<{
[dispatch, statusId], [dispatch, statusId],
); );
if (!statusId) return null; if (!statusId || isFiltered) return null;
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>

View file

@ -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 ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star.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 { replyCompose } from 'mastodon/actions/compose';
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions'; import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
@ -159,22 +161,26 @@ class Footer extends ImmutablePureComponent {
replyTitle = intl.formatMessage(messages.replyAll); replyTitle = intl.formatMessage(messages.replyAll);
} }
let reblogTitle = ''; let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) { if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private); reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus ? RepeatIcon : RepeatPrivateIcon;
} else if (publicStatus) { } else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog); reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) { } else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private); reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else { } else {
reblogTitle = intl.formatMessage(messages.cannot_reblog); reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
} }
return ( return (
<div className='picture-in-picture__footer'> <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='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')} /> <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')}`} />} {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> </div>

View file

@ -116,6 +116,7 @@ export const MuteModal = ({ accountId, acct }) => {
<div className='safety-action-modal__bottom__collapsible'> <div className='safety-action-modal__bottom__collapsible'>
<div className='safety-action-modal__field-group'> <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='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='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='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} /> <RadioButtonLabel name='duration' value='2592000' label={intl.formatMessage(messages.days, { number: 30 })} currentValue={muteDuration} onChange={handleChangeMuteDuration} />

View file

@ -157,7 +157,6 @@
"compose_form.poll.duration": "مُدَّة اِستطلاع الرأي", "compose_form.poll.duration": "مُدَّة اِستطلاع الرأي",
"compose_form.poll.multiple": "متعدد الخيارات", "compose_form.poll.multiple": "متعدد الخيارات",
"compose_form.poll.option_placeholder": "الخيار {number}", "compose_form.poll.option_placeholder": "الخيار {number}",
"compose_form.poll.single": "اختر واحدا",
"compose_form.poll.switch_to_multiple": "تغيِير الاستطلاع للسماح باِخيارات مُتعدِّدة", "compose_form.poll.switch_to_multiple": "تغيِير الاستطلاع للسماح باِخيارات مُتعدِّدة",
"compose_form.poll.switch_to_single": "تغيِير الاستطلاع للسماح باِخيار واحد فقط", "compose_form.poll.switch_to_single": "تغيِير الاستطلاع للسماح باِخيار واحد فقط",
"compose_form.poll.type": "الطراز", "compose_form.poll.type": "الطراز",

View file

@ -29,7 +29,7 @@
"account.followers": "Siguidores", "account.followers": "Siguidores",
"account.followers.empty": "Naide sigue a esti perfil.", "account.followers.empty": "Naide sigue a esti perfil.",
"account.follows.empty": "Esti perfil nun sigue a naide.", "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.in_memoriam": "N'alcordanza.",
"account.joined_short": "Data de xunión", "account.joined_short": "Data de xunión",
"account.link_verified_on": "La propiedá d'esti enllaz comprobóse'l {date}", "account.link_verified_on": "La propiedá d'esti enllaz comprobóse'l {date}",
@ -122,6 +122,7 @@
"conversation.open": "Ver la conversación", "conversation.open": "Ver la conversación",
"conversation.with": "Con {names}", "conversation.with": "Con {names}",
"copypaste.copied": "Copióse", "copypaste.copied": "Copióse",
"copypaste.copy_to_clipboard": "Copiar nel cartafueyu",
"directory.federated": "Del fediversu conocíu", "directory.federated": "Del fediversu conocíu",
"directory.local": "De «{domain}» namás", "directory.local": "De «{domain}» namás",
"directory.new_arrivals": "Cuentes nueves", "directory.new_arrivals": "Cuentes nueves",
@ -130,7 +131,7 @@
"dismissable_banner.dismiss": "Escartar", "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.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.", "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:", "embed.preview": "Va apaecer asina:",
"emoji_button.activity": "Actividá", "emoji_button.activity": "Actividá",
"emoji_button.flags": "Banderes", "emoji_button.flags": "Banderes",
@ -251,7 +252,7 @@
"keyboard_shortcuts.requests": "Abrir la llista de solicitúes de siguimientu", "keyboard_shortcuts.requests": "Abrir la llista de solicitúes de siguimientu",
"keyboard_shortcuts.search": "Enfocar la barra de busca", "keyboard_shortcuts.search": "Enfocar la barra de busca",
"keyboard_shortcuts.start": "Abrir la columna «Entamar»", "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.toot": "Comenzar un artículu nuevu",
"keyboard_shortcuts.unfocus": "Desenfocar l'área de composición/busca", "keyboard_shortcuts.unfocus": "Desenfocar l'área de composición/busca",
"keyboard_shortcuts.up": "Xubir na llista", "keyboard_shortcuts.up": "Xubir na llista",
@ -299,6 +300,7 @@
"notifications.column_settings.admin.sign_up": "Rexistros nuevos:", "notifications.column_settings.admin.sign_up": "Rexistros nuevos:",
"notifications.column_settings.follow": "Siguidores nuevos:", "notifications.column_settings.follow": "Siguidores nuevos:",
"notifications.column_settings.follow_request": "Solicitúes de siguimientu nueves:", "notifications.column_settings.follow_request": "Solicitúes de siguimientu nueves:",
"notifications.column_settings.group": "Agrupar",
"notifications.column_settings.mention": "Menciones:", "notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.poll": "Resultaos de les encuestes:", "notifications.column_settings.poll": "Resultaos de les encuestes:",
"notifications.column_settings.reblog": "Artículos compartíos:", "notifications.column_settings.reblog": "Artículos compartíos:",
@ -418,11 +420,12 @@
"status.direct": "Mentar a @{name} per privao", "status.direct": "Mentar a @{name} per privao",
"status.direct_indicator": "Mención privada", "status.direct_indicator": "Mención privada",
"status.edited_x_times": "Editóse {count, plural, one {{count} vegada} other {{count} vegaes}}", "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.filter": "Peñerar esti artículu",
"status.history.created": "{name} creó {date}", "status.history.created": "{name} creó {date}",
"status.history.edited": "{name} editó {date}", "status.history.edited": "{name} editó {date}",
"status.load_more": "Cargar más", "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.mention": "Mentar a @{name}",
"status.more": "Más", "status.more": "Más",
"status.mute": "Desactivar los avisos de @{name}", "status.mute": "Desactivar los avisos de @{name}",
@ -468,14 +471,14 @@
"upload_modal.applying": "Aplicando…", "upload_modal.applying": "Aplicando…",
"upload_modal.detect_text": "Detectar el testu de la semeya", "upload_modal.detect_text": "Detectar el testu de la semeya",
"upload_modal.edit_media": "Edición", "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.label": "Xubiendo…",
"upload_progress.processing": "Procesando…", "upload_progress.processing": "Procesando…",
"video.close": "Zarrar el videu", "video.close": "Zarrar el videu",
"video.download": "Baxar el ficheru", "video.download": "Baxar el ficheru",
"video.expand": "Espander el videu", "video.expand": "Espander el videu",
"video.fullscreen": "Pantalla completa", "video.fullscreen": "Pantalla completa",
"video.hide": "Anubrir el videu", "video.hide": "Esconder el videu",
"video.mute": "Desactivar el soníu", "video.mute": "Desactivar el soníu",
"video.pause": "Posar", "video.pause": "Posar",
"video.play": "Reproducir", "video.play": "Reproducir",

View file

@ -156,7 +156,6 @@
"compose_form.poll.duration": "Працягласць апытання", "compose_form.poll.duration": "Працягласць апытання",
"compose_form.poll.multiple": "Множны выбар", "compose_form.poll.multiple": "Множны выбар",
"compose_form.poll.option_placeholder": "Варыянт {number}", "compose_form.poll.option_placeholder": "Варыянт {number}",
"compose_form.poll.single": "Адзін варыянт",
"compose_form.poll.switch_to_multiple": "Змяніце апытанне, каб дазволіць некалькі варыянтаў адказу", "compose_form.poll.switch_to_multiple": "Змяніце апытанне, каб дазволіць некалькі варыянтаў адказу",
"compose_form.poll.switch_to_single": "Змяніце апытанне, каб дазволіць адзіны варыянт адказу", "compose_form.poll.switch_to_single": "Змяніце апытанне, каб дазволіць адзіны варыянт адказу",
"compose_form.poll.type": "Стыль", "compose_form.poll.type": "Стыль",

View file

@ -158,7 +158,6 @@
"compose_form.poll.duration": "Времетраене на анкетата", "compose_form.poll.duration": "Времетраене на анкетата",
"compose_form.poll.multiple": "Множествен избор", "compose_form.poll.multiple": "Множествен избор",
"compose_form.poll.option_placeholder": "Избор {number}", "compose_form.poll.option_placeholder": "Избор {number}",
"compose_form.poll.single": "Подберете нещо",
"compose_form.poll.switch_to_multiple": "Промяна на анкетата, за да се позволят множество възможни избора", "compose_form.poll.switch_to_multiple": "Промяна на анкетата, за да се позволят множество възможни избора",
"compose_form.poll.switch_to_single": "Промяна на анкетата, за да се позволи един възможен избор", "compose_form.poll.switch_to_single": "Промяна на анкетата, за да се позволи един възможен избор",
"compose_form.poll.type": "Стил", "compose_form.poll.type": "Стил",
@ -490,7 +489,6 @@
"notification.favourite": "{name} направи любима публикацията ви", "notification.favourite": "{name} направи любима публикацията ви",
"notification.favourite.name_and_others_with_link": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> направиха любима ваша публикация", "notification.favourite.name_and_others_with_link": "{name} и <a>{count, plural, one {# друг} other {# други}}</a> направиха любима ваша публикация",
"notification.follow": "{name} ви последва", "notification.follow": "{name} ви последва",
"notification.follow.name_and_others": "{name} и {count, plural, one {# друг} other {# други}} ви последваха",
"notification.follow_request": "{name} поиска да ви последва", "notification.follow_request": "{name} поиска да ви последва",
"notification.follow_request.name_and_others": "{name} и {count, plural, one {# друг} other {# други}} поискаха да ви последват", "notification.follow_request.name_and_others": "{name} и {count, plural, one {# друг} other {# други}} поискаха да ви последват",
"notification.label.mention": "Споменаване", "notification.label.mention": "Споменаване",

View file

@ -145,7 +145,6 @@
"compose_form.poll.duration": "Pad ar sontadeg", "compose_form.poll.duration": "Pad ar sontadeg",
"compose_form.poll.multiple": "Meur a choaz", "compose_form.poll.multiple": "Meur a choaz",
"compose_form.poll.option_placeholder": "Choaz {number}", "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_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.switch_to_single": "Kemmañ ar sontadeg evit aotren un dibab hepken",
"compose_form.poll.type": "Neuz", "compose_form.poll.type": "Neuz",
@ -385,6 +384,7 @@
"notification.admin.report": "Disklêriet eo bet {target} gant {name}", "notification.admin.report": "Disklêriet eo bet {target} gant {name}",
"notification.admin.sign_up": "{name} en·he deus lakaet e·hec'h anv", "notification.admin.sign_up": "{name} en·he deus lakaet e·hec'h anv",
"notification.follow": "heuliañ a ra {name} ac'hanoc'h", "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.follow_request": "Gant {name} eo bet goulennet ho heuliañ",
"notification.moderation-warning.learn_more": "Gouzout hiroc'h", "notification.moderation-warning.learn_more": "Gouzout hiroc'h",
"notification.own_poll": "Echu eo ho sontadeg", "notification.own_poll": "Echu eo ho sontadeg",
@ -399,6 +399,7 @@
"notifications.column_settings.favourite": "Muiañ-karet:", "notifications.column_settings.favourite": "Muiañ-karet:",
"notifications.column_settings.follow": "Heulierien nevez:", "notifications.column_settings.follow": "Heulierien nevez:",
"notifications.column_settings.follow_request": "Pedadoù heuliañ nevez :", "notifications.column_settings.follow_request": "Pedadoù heuliañ nevez :",
"notifications.column_settings.group": "Strollañ",
"notifications.column_settings.mention": "Menegoù:", "notifications.column_settings.mention": "Menegoù:",
"notifications.column_settings.poll": "Disoc'hoù ar sontadeg:", "notifications.column_settings.poll": "Disoc'hoù ar sontadeg:",
"notifications.column_settings.push": "Kemennoù push", "notifications.column_settings.push": "Kemennoù push",

View file

@ -158,7 +158,6 @@
"compose_form.poll.duration": "Durada de l'enquesta", "compose_form.poll.duration": "Durada de l'enquesta",
"compose_form.poll.multiple": "Opcions múltiples", "compose_form.poll.multiple": "Opcions múltiples",
"compose_form.poll.option_placeholder": "Opció {number}", "compose_form.poll.option_placeholder": "Opció {number}",
"compose_form.poll.single": "Trieu-ne una",
"compose_form.poll.switch_to_multiple": "Canvia lenquesta per a permetre múltiples opcions", "compose_form.poll.switch_to_multiple": "Canvia lenquesta per a permetre múltiples opcions",
"compose_form.poll.switch_to_single": "Canvia lenquesta per a permetre una única opció", "compose_form.poll.switch_to_single": "Canvia lenquesta per a permetre una única opció",
"compose_form.poll.type": "Estil", "compose_form.poll.type": "Estil",
@ -508,7 +507,6 @@
"notification.favourite": "{name} ha afavorit el teu tut", "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.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} 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} 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.follow_request.name_and_others": "{name} i {count, plural, one {# altre} other {# altres}} han demanat de seguir-vos",
"notification.label.mention": "Menció", "notification.label.mention": "Menció",

View file

@ -143,7 +143,6 @@
"compose_form.poll.duration": "ماوەی ڕاپرسی", "compose_form.poll.duration": "ماوەی ڕاپرسی",
"compose_form.poll.multiple": "فرە هەڵبژاردە", "compose_form.poll.multiple": "فرە هەڵبژاردە",
"compose_form.poll.option_placeholder": "بژاردەی {number}", "compose_form.poll.option_placeholder": "بژاردەی {number}",
"compose_form.poll.single": "یەکێك هەلبژێرە",
"compose_form.poll.switch_to_multiple": "ڕاپرسی بگۆڕە بۆ ڕێگەدان بە چەند هەڵبژاردنێک", "compose_form.poll.switch_to_multiple": "ڕاپرسی بگۆڕە بۆ ڕێگەدان بە چەند هەڵبژاردنێک",
"compose_form.poll.switch_to_single": "گۆڕینی ڕاپرسی بۆ ڕێگەدان بە تاکە هەڵبژاردنێک", "compose_form.poll.switch_to_single": "گۆڕینی ڕاپرسی بۆ ڕێگەدان بە تاکە هەڵبژاردنێک",
"compose_form.poll.type": "ستایڵ", "compose_form.poll.type": "ستایڵ",

View file

@ -157,7 +157,6 @@
"compose_form.poll.duration": "Doba trvání ankety", "compose_form.poll.duration": "Doba trvání ankety",
"compose_form.poll.multiple": "Výběr z více možností", "compose_form.poll.multiple": "Výběr z více možností",
"compose_form.poll.option_placeholder": "Volba {number}", "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_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.switch_to_single": "Povolit u ankety výběr pouze jedné volby",
"compose_form.poll.type": "Styl", "compose_form.poll.type": "Styl",

View file

@ -158,7 +158,7 @@
"compose_form.poll.duration": "Cyfnod pleidlais", "compose_form.poll.duration": "Cyfnod pleidlais",
"compose_form.poll.multiple": "Dewis lluosog", "compose_form.poll.multiple": "Dewis lluosog",
"compose_form.poll.option_placeholder": "Dewis {number}", "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_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.switch_to_single": "Newid pleidlais i gyfyngu i un dewis",
"compose_form.poll.type": "Arddull", "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_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.they_wont_know": "Fyddan nhw ddim yn gwybod eu bod wedi cael eu blocio.",
"domain_block_modal.title": "Blocio parth?", "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_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_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.", "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": "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.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": "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": "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.follow_request.name_and_others": "Mae {name} a{count, plural, one {# other} other {# others}} wedi gofyn i'ch dilyn chi",
"notification.label.mention": "Crybwyll", "notification.label.mention": "Crybwyll",
@ -515,6 +516,7 @@
"notification.label.private_reply": "Ateb preifat", "notification.label.private_reply": "Ateb preifat",
"notification.label.reply": "Ateb", "notification.label.reply": "Ateb",
"notification.mention": "Crybwyll", "notification.mention": "Crybwyll",
"notification.mentioned_you": "Rydych wedi'ch crybwyll gan {name}",
"notification.moderation-warning.learn_more": "Dysgu mwy", "notification.moderation-warning.learn_more": "Dysgu mwy",
"notification.moderation_warning": "Rydych wedi derbyn rhybudd gan gymedrolwr", "notification.moderation_warning": "Rydych wedi derbyn rhybudd gan gymedrolwr",
"notification.moderation_warning.action_delete_statuses": "Mae rhai o'ch postiadau wedi'u dileu.", "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.filter_bar.category": "Bar hidlo cyflym",
"notifications.column_settings.follow": "Dilynwyr newydd:", "notifications.column_settings.follow": "Dilynwyr newydd:",
"notifications.column_settings.follow_request": "Ceisiadau dilyn newydd:", "notifications.column_settings.follow_request": "Ceisiadau dilyn newydd:",
"notifications.column_settings.group": "Grŵp",
"notifications.column_settings.mention": "Crybwylliadau:", "notifications.column_settings.mention": "Crybwylliadau:",
"notifications.column_settings.poll": "Canlyniadau pleidlais:", "notifications.column_settings.poll": "Canlyniadau pleidlais:",
"notifications.column_settings.push": "Hysbysiadau gwthiadwy", "notifications.column_settings.push": "Hysbysiadau gwthiadwy",

View file

@ -158,7 +158,6 @@
"compose_form.poll.duration": "Afstemningens varighed", "compose_form.poll.duration": "Afstemningens varighed",
"compose_form.poll.multiple": "Multivalg", "compose_form.poll.multiple": "Multivalg",
"compose_form.poll.option_placeholder": "Valgmulighed {number}", "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_multiple": "Ændr afstemning til flervalgstype",
"compose_form.poll.switch_to_single": "Ændr afstemning til enkeltvalgstype", "compose_form.poll.switch_to_single": "Ændr afstemning til enkeltvalgstype",
"compose_form.poll.type": "Stil", "compose_form.poll.type": "Stil",
@ -508,7 +507,7 @@
"notification.favourite": "{name} favoritmarkerede dit indlæg", "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.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} 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} 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.follow_request.name_and_others": "{name} og {count, plural, one {# anden} other {# andre}} har anmodet om at følger dig",
"notification.label.mention": "Omtale", "notification.label.mention": "Omtale",
@ -567,6 +566,7 @@
"notifications.column_settings.filter_bar.category": "Hurtigfiltreringsbjælke", "notifications.column_settings.filter_bar.category": "Hurtigfiltreringsbjælke",
"notifications.column_settings.follow": "Nye følgere:", "notifications.column_settings.follow": "Nye følgere:",
"notifications.column_settings.follow_request": "Nye følgeanmodninger:", "notifications.column_settings.follow_request": "Nye følgeanmodninger:",
"notifications.column_settings.group": "Gruppere",
"notifications.column_settings.mention": "Omtaler:", "notifications.column_settings.mention": "Omtaler:",
"notifications.column_settings.poll": "Afstemningsresultater:", "notifications.column_settings.poll": "Afstemningsresultater:",
"notifications.column_settings.push": "Push-notifikationer", "notifications.column_settings.push": "Push-notifikationer",

View file

@ -62,7 +62,7 @@
"account.requested_follow": "{name} möchte dir folgen", "account.requested_follow": "{name} möchte dir folgen",
"account.share": "Profil von @{name} teilen", "account.share": "Profil von @{name} teilen",
"account.show_reblogs": "Geteilte Beiträge von @{name} anzeigen", "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": "Blockierung von @{name} aufheben",
"account.unblock_domain": "Blockierung von {domain} aufheben", "account.unblock_domain": "Blockierung von {domain} aufheben",
"account.unblock_short": "Blockierung aufheben", "account.unblock_short": "Blockierung aufheben",
@ -508,7 +508,7 @@
"notification.favourite": "{name} favorisierte deinen Beitrag", "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.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} 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} 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.follow_request.name_and_others": "{name} und {count, plural, one {# weitere Person} other {# weitere Personen}} möchten dir folgen",
"notification.label.mention": "Erwähnung", "notification.label.mention": "Erwähnung",
@ -567,6 +567,7 @@
"notifications.column_settings.filter_bar.category": "Filterleiste", "notifications.column_settings.filter_bar.category": "Filterleiste",
"notifications.column_settings.follow": "Neue Follower:", "notifications.column_settings.follow": "Neue Follower:",
"notifications.column_settings.follow_request": "Neue Follower-Anfragen:", "notifications.column_settings.follow_request": "Neue Follower-Anfragen:",
"notifications.column_settings.group": "Gruppieren",
"notifications.column_settings.mention": "Erwähnungen:", "notifications.column_settings.mention": "Erwähnungen:",
"notifications.column_settings.poll": "Umfrageergebnisse:", "notifications.column_settings.poll": "Umfrageergebnisse:",
"notifications.column_settings.push": "Push-Benachrichtigungen", "notifications.column_settings.push": "Push-Benachrichtigungen",

View file

@ -158,7 +158,6 @@
"compose_form.poll.duration": "Διάρκεια δημοσκόπησης", "compose_form.poll.duration": "Διάρκεια δημοσκόπησης",
"compose_form.poll.multiple": "Πολλαπλή επιλογή", "compose_form.poll.multiple": "Πολλαπλή επιλογή",
"compose_form.poll.option_placeholder": "Επιλογή {number}", "compose_form.poll.option_placeholder": "Επιλογή {number}",
"compose_form.poll.single": "Διάλεξε ένα",
"compose_form.poll.switch_to_multiple": "Ενημέρωση δημοσκόπησης με πολλαπλές επιλογές", "compose_form.poll.switch_to_multiple": "Ενημέρωση δημοσκόπησης με πολλαπλές επιλογές",
"compose_form.poll.switch_to_single": "Ενημέρωση δημοσκόπησης με μοναδική επιλογή", "compose_form.poll.switch_to_single": "Ενημέρωση δημοσκόπησης με μοναδική επιλογή",
"compose_form.poll.type": "Στυλ", "compose_form.poll.type": "Στυλ",
@ -508,7 +507,6 @@
"notification.favourite": "{name} favorited your post\n{name} προτίμησε την ανάρτηση σου", "notification.favourite": "{name} favorited your post\n{name} προτίμησε την ανάρτηση σου",
"notification.favourite.name_and_others_with_link": "{name} και <a>{count, plural, one {# ακόμη} other {# ακόμη}}</a> αγάπησαν την ανάρτησή σου", "notification.favourite.name_and_others_with_link": "{name} και <a>{count, plural, one {# ακόμη} other {# ακόμη}}</a> αγάπησαν την ανάρτησή σου",
"notification.follow": "Ο/Η {name} σε ακολούθησε", "notification.follow": "Ο/Η {name} σε ακολούθησε",
"notification.follow.name_and_others": "{name} και {count, plural, one {# ακόμη} other {# ακόμη}} σε ακολούθησαν",
"notification.follow_request": "Ο/H {name} ζήτησε να σε ακολουθήσει", "notification.follow_request": "Ο/H {name} ζήτησε να σε ακολουθήσει",
"notification.follow_request.name_and_others": "{name} και {count, plural, one {# άλλος} other {# άλλοι}} ζήτησαν να σε ακολουθήσουν", "notification.follow_request.name_and_others": "{name} και {count, plural, one {# άλλος} other {# άλλοι}} ζήτησαν να σε ακολουθήσουν",
"notification.label.mention": "Επισήμανση", "notification.label.mention": "Επισήμανση",

View file

@ -158,7 +158,6 @@
"compose_form.poll.duration": "Poll duration", "compose_form.poll.duration": "Poll duration",
"compose_form.poll.multiple": "Multiple choice", "compose_form.poll.multiple": "Multiple choice",
"compose_form.poll.option_placeholder": "Option {number}", "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_multiple": "Change poll to allow multiple choices",
"compose_form.poll.switch_to_single": "Change poll to allow for a single choice", "compose_form.poll.switch_to_single": "Change poll to allow for a single choice",
"compose_form.poll.type": "Style", "compose_form.poll.type": "Style",
@ -508,7 +507,6 @@
"notification.favourite": "{name} favourited your post", "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.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} 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} 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.follow_request.name_and_others": "{name} and {count, plural, one {# other} other {# others}} has requested to follow you",
"notification.label.mention": "Mention", "notification.label.mention": "Mention",
@ -791,7 +789,7 @@
"status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}", "status.edited_x_times": "Edited {count, plural, one {{count} time} other {{count} times}}",
"status.embed": "Get embed code", "status.embed": "Get embed code",
"status.favourite": "Favourite", "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.filter": "Filter this post",
"status.history.created": "{name} created {date}", "status.history.created": "{name} created {date}",
"status.history.edited": "{name} edited {date}", "status.history.edited": "{name} edited {date}",

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