mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-16 09:18:46 +00:00
Compare commits
283 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1ff48978d | ||
|
|
24304fbbe6 | ||
|
|
644caeb156 | ||
|
|
ad92660de6 | ||
|
|
c5d17a3997 | ||
|
|
a8613b7cda | ||
|
|
0c2fa2aab4 | ||
|
|
62f019252a | ||
|
|
4228ca614c | ||
|
|
7e20ee7695 | ||
|
|
1ed1cdba1b | ||
|
|
b73e968641 | ||
|
|
559f7a8e61 | ||
|
|
bcfd6ab3e4 | ||
|
|
1704a7d858 | ||
|
|
97fd14e141 | ||
|
|
c1f398ae93 | ||
|
|
19b3469c29 | ||
|
|
57e4232b3e | ||
|
|
c6b501c42d | ||
|
|
5140f31cbb | ||
|
|
adee65ad1b | ||
|
|
fba7e85b9b | ||
|
|
bc95675236 | ||
|
|
ccc4fcbdb8 | ||
|
|
e3afbab115 | ||
|
|
baac429103 | ||
|
|
b1a584d252 | ||
|
|
8787077462 | ||
|
|
10bcbf15af | ||
|
|
fb29ac0f5f | ||
|
|
b0f88be86f | ||
|
|
018b85e767 | ||
|
|
08d2250ad2 | ||
|
|
679e7555ee | ||
|
|
452153d55d | ||
|
|
2954c2facb | ||
|
|
44e38b79de | ||
|
|
b32a67ff74 | ||
|
|
4f33b041f0 | ||
|
|
6e906884cf | ||
|
|
317715254f | ||
|
|
2b148d3e88 | ||
|
|
227d48dbd5 | ||
|
|
94fed6e140 | ||
|
|
37b029d400 | ||
|
|
11baa26db2 | ||
|
|
c7172b54fe | ||
|
|
74496838e7 | ||
|
|
ca39069433 | ||
|
|
7ad9581940 | ||
|
|
e4f2a054c9 | ||
|
|
68eb62f4a9 | ||
|
|
e63d0cfe85 | ||
|
|
4da31b8263 | ||
|
|
17695ace33 | ||
|
|
fa2625a0d9 | ||
|
|
1005b2f7b2 | ||
|
|
f24b0e9505 | ||
|
|
4db64491ee | ||
|
|
fd79e2417d | ||
|
|
96455304bc | ||
|
|
63f4e2070c | ||
|
|
faed9bf9f1 | ||
|
|
10f10844ff | ||
|
|
5c8d2be23b | ||
|
|
90072f4367 | ||
|
|
512bfc0a54 | ||
|
|
d764ae017d | ||
|
|
757aed3290 | ||
|
|
3cff7caffd | ||
|
|
533477e77c | ||
|
|
afcfc64007 | ||
|
|
734f0dd182 | ||
|
|
bcc798d6a7 | ||
|
|
3a4242ce01 | ||
|
|
23376cb691 | ||
|
|
c2d65f7142 | ||
|
|
13ab4b54e2 | ||
|
|
df0b641914 | ||
|
|
624b942c2e | ||
|
|
cfa2e0503a | ||
|
|
de945eef63 | ||
|
|
9a7030fb69 | ||
|
|
221da1ba04 | ||
|
|
ff85540904 | ||
|
|
c2862049a2 | ||
|
|
0d69cc068c | ||
|
|
26f25ef4ba | ||
|
|
3b4070cfcc | ||
|
|
eb997c9f0e | ||
|
|
4239baa1f4 | ||
|
|
5532d1c2cb | ||
|
|
3f0d90f019 | ||
|
|
15e1a63e4a | ||
|
|
6b8ff1cf6e | ||
|
|
6cbd217055 | ||
|
|
90c7c1bf7d | ||
|
|
e06448e652 | ||
|
|
3752db3c9a | ||
|
|
cc5c125cc7 | ||
|
|
f65523c5b6 | ||
|
|
5b6b23eeef | ||
|
|
0cbf03efa7 | ||
|
|
90f2c7a1e9 | ||
|
|
f0d734cc6e | ||
|
|
0720ef5f62 | ||
|
|
dc9a106d4c | ||
|
|
c634da32cf | ||
|
|
2d8ce9e19a | ||
|
|
1ddf1aedf1 | ||
|
|
931870ca34 | ||
|
|
7f9b0f36ba | ||
|
|
dd0992b25d | ||
|
|
9b677f099e | ||
|
|
c13b8026f0 | ||
|
|
bf1375ae37 | ||
|
|
b06161dba3 | ||
|
|
a089109b77 | ||
|
|
74f9f7c600 | ||
|
|
ea1b598246 | ||
|
|
dbedd021f5 | ||
|
|
5d79af928c | ||
|
|
a62be22cb1 | ||
|
|
96ffbc05c0 | ||
|
|
39fb314421 | ||
|
|
90f6984ff1 | ||
|
|
9adb96f3a1 | ||
|
|
d5a3478864 | ||
|
|
9877a053f6 | ||
|
|
605ed50603 | ||
|
|
e1609c6813 | ||
|
|
de5d6e98ae | ||
|
|
2b0410f903 | ||
|
|
f7aab0cc2f | ||
|
|
de5f522cc0 | ||
|
|
d728fa9991 | ||
|
|
044dd3f788 | ||
|
|
afc440435c | ||
|
|
d0fb7939bb | ||
|
|
7388a6ce9a | ||
|
|
cd2a3bac79 | ||
|
|
f0e011fbc9 | ||
|
|
acbc273d6e | ||
|
|
1f0c84749d | ||
|
|
e507b4f884 | ||
|
|
93348136a5 | ||
|
|
3a5e83b91a | ||
|
|
8d37565c19 | ||
|
|
177e8fe972 | ||
|
|
198283a188 | ||
|
|
36452845d7 | ||
|
|
5c4bcd2f08 | ||
|
|
a20f38c930 | ||
|
|
b01bd74698 | ||
|
|
41e342a88f | ||
|
|
9258ee8847 | ||
|
|
6d72c13a4d | ||
|
|
ad4be12473 | ||
|
|
527d1253bf | ||
|
|
ae676edc2b | ||
|
|
63df649fe5 | ||
|
|
0ff427fab3 | ||
|
|
dc2f9eef77 | ||
|
|
ff1247ad16 | ||
|
|
fbe55a4545 | ||
|
|
a72819660a | ||
|
|
c292ed07fe | ||
|
|
2d008108a4 | ||
|
|
0c59ef44b1 | ||
|
|
12297faa1d | ||
|
|
9b6f92e47f | ||
|
|
8b6247ca44 | ||
|
|
836cbca469 | ||
|
|
b091e531a5 | ||
|
|
49b3d5692e | ||
|
|
70472de726 | ||
|
|
304e440f88 | ||
|
|
ca68a3cacb | ||
|
|
066efc2d3f | ||
|
|
a2e24ee2de | ||
|
|
ee61f7772a | ||
|
|
5ee72f0e2d | ||
|
|
192e9d16eb | ||
|
|
a3f40309fb | ||
|
|
782a785893 | ||
|
|
9f165436d2 | ||
|
|
592945e498 | ||
|
|
bfb610922d | ||
|
|
480dcecc11 | ||
|
|
2647606a15 | ||
|
|
b40adb4a89 | ||
|
|
f99da81ef8 | ||
|
|
799f507dce | ||
|
|
81472396bc | ||
|
|
a295832960 | ||
|
|
e018e6321f | ||
|
|
f75eb1a8b0 | ||
|
|
de4f7859b4 | ||
|
|
e5e0144957 | ||
|
|
45a520603b | ||
|
|
6ac78ead52 | ||
|
|
c0d3b3de10 | ||
|
|
9e04e46521 | ||
|
|
fa4a82326d | ||
|
|
93fa102f9a | ||
|
|
9ee86a738e | ||
|
|
e272cf5983 | ||
|
|
4382de310c | ||
|
|
94c69bba25 | ||
|
|
ab36c152f9 | ||
|
|
fc5b558b32 | ||
|
|
77ff94d3d2 | ||
|
|
959841ae95 | ||
|
|
f669493d96 | ||
|
|
83b3c50778 | ||
|
|
dc7a42551f | ||
|
|
4a859140ec | ||
|
|
edcf3d9234 | ||
|
|
cae93e79a4 | ||
|
|
83a98cb81a | ||
|
|
889edc560a | ||
|
|
2e0d918d7d | ||
|
|
3b4312476f | ||
|
|
4fba4f8c82 | ||
|
|
25de2f57ee | ||
|
|
026643ab24 | ||
|
|
61e3e81e28 | ||
|
|
354f54907d | ||
|
|
4d611e94ee | ||
|
|
a09a26da49 | ||
|
|
59a8066045 | ||
|
|
3cad5095c9 | ||
|
|
e58d99a771 | ||
|
|
69c76fd94a | ||
|
|
1b6bd585ab | ||
|
|
dfe851b476 | ||
|
|
6d5aa58f88 | ||
|
|
81cd489208 | ||
|
|
55b5364534 | ||
|
|
2e8b752c55 | ||
|
|
d82ffdccbb | ||
|
|
5c72b46a4e | ||
|
|
aa46348c03 | ||
|
|
404f467fcf | ||
|
|
4a2d3929c5 | ||
|
|
ceba0f082e | ||
|
|
7de8d5ffca | ||
|
|
74291dfb77 | ||
|
|
f07707a9bb | ||
|
|
931553844d | ||
|
|
243a85ec8d | ||
|
|
cbf1349370 | ||
|
|
b8fdffe824 | ||
|
|
c91e06bcad | ||
|
|
b2ce9bb4c7 | ||
|
|
19d1392b33 | ||
|
|
09cf617d7f | ||
|
|
784d1bfb29 | ||
|
|
754b03d8cb | ||
|
|
f397550311 | ||
|
|
97db4bd4dd | ||
|
|
1e19242134 | ||
|
|
4e6f13a0fb | ||
|
|
f517f0dbef | ||
|
|
53624b1b54 | ||
|
|
a473988969 | ||
|
|
4ad1e955eb | ||
|
|
66ef4b9984 | ||
|
|
ce2481a81b | ||
|
|
efa74a6c44 | ||
|
|
bdceb1dacf | ||
|
|
e13453aec4 | ||
|
|
25e8a6eaeb | ||
|
|
c828e7731c | ||
|
|
6734b6550f | ||
|
|
6398d7b784 | ||
|
|
1283c3544c | ||
|
|
8ac00533ff | ||
|
|
1b3472bec8 | ||
|
|
c8df7f4995 | ||
|
|
94743fea2c | ||
|
|
deee164acf |
@@ -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
|
||||||
# --------
|
# --------
|
||||||
|
|||||||
1
.github/actions/setup-ruby/action.yml
vendored
1
.github/actions/setup-ruby/action.yml
vendored
@@ -21,3 +21,4 @@ runs:
|
|||||||
with:
|
with:
|
||||||
ruby-version: ${{ inputs.ruby-version }}
|
ruby-version: ${{ inputs.ruby-version }}
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
|
cache-version: 4.3
|
||||||
|
|||||||
164
.github/workflows/build-container-image.yml
vendored
164
.github/workflows/build-container-image.yml
vendored
@@ -1,14 +1,9 @@
|
|||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
inputs:
|
inputs:
|
||||||
platforms:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
cache:
|
cache:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
use_native_arm64_builder:
|
|
||||||
type: boolean
|
|
||||||
push_to_images:
|
push_to_images:
|
||||||
type: string
|
type: string
|
||||||
version_prerelease:
|
version_prerelease:
|
||||||
@@ -24,42 +19,36 @@ on:
|
|||||||
file_to_build:
|
file_to_build:
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
# This builds multiple images with one runner each, allowing us to build for multiple architectures
|
||||||
|
# using Github's runners.
|
||||||
|
# The two-step process is adapted form:
|
||||||
|
# https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
|
||||||
jobs:
|
jobs:
|
||||||
|
# Build each (amd64 and arm64) image separately
|
||||||
build-image:
|
build-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- linux/amd64
|
||||||
|
- linux/arm64
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: docker/setup-qemu-action@v3
|
- name: Prepare
|
||||||
if: contains(inputs.platforms, 'linux/arm64') && !inputs.use_native_arm64_builder
|
env:
|
||||||
|
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
# Transform multi-line variable into comma-separated variable
|
||||||
|
image_names=${PUSH_TO_IMAGES//$'\n'/,}
|
||||||
|
echo "IMAGE_NAMES=${image_names%,}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v3
|
- uses: docker/setup-buildx-action@v3
|
||||||
id: buildx
|
id: buildx
|
||||||
if: ${{ !(inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')) }}
|
|
||||||
|
|
||||||
- name: Start a local Docker Builder
|
|
||||||
if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')
|
|
||||||
run: |
|
|
||||||
docker run --rm -d --name buildkitd -p 1234:1234 --privileged moby/buildkit:latest --addr tcp://0.0.0.0:1234
|
|
||||||
|
|
||||||
- uses: docker/setup-buildx-action@v3
|
|
||||||
id: buildx-native
|
|
||||||
if: inputs.use_native_arm64_builder && contains(inputs.platforms, 'linux/arm64')
|
|
||||||
with:
|
|
||||||
driver: remote
|
|
||||||
endpoint: tcp://localhost:1234
|
|
||||||
platforms: linux/amd64
|
|
||||||
append: |
|
|
||||||
- endpoint: tcp://${{ vars.DOCKER_BUILDER_HETZNER_ARM64_01_HOST }}:13865
|
|
||||||
platforms: linux/arm64
|
|
||||||
name: mastodon-docker-builder-arm64-01
|
|
||||||
driver-opts:
|
|
||||||
- servername=mastodon-docker-builder-arm64-01
|
|
||||||
env:
|
|
||||||
BUILDER_NODE_1_AUTH_TLS_CACERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CACERT }}
|
|
||||||
BUILDER_NODE_1_AUTH_TLS_CERT: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_CERT }}
|
|
||||||
BUILDER_NODE_1_AUTH_TLS_KEY: ${{ secrets.DOCKER_BUILDER_HETZNER_ARM64_01_KEY }}
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
if: contains(inputs.push_to_images, 'tootsuite')
|
if: contains(inputs.push_to_images, 'tootsuite')
|
||||||
@@ -76,8 +65,91 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: docker/metadata-action@v5
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
if: ${{ inputs.push_to_images != '' }}
|
||||||
|
with:
|
||||||
|
images: ${{ inputs.push_to_images }}
|
||||||
|
flavor: ${{ inputs.flavor }}
|
||||||
|
labels: ${{ inputs.labels }}
|
||||||
|
|
||||||
|
- name: Build and push by digest
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ${{ inputs.file_to_build }}
|
||||||
|
build-args: |
|
||||||
|
MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
|
||||||
|
MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
|
||||||
|
SOURCE_COMMIT=${{ github.sha }}
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
provenance: false
|
||||||
|
push: ${{ inputs.push_to_images != '' }}
|
||||||
|
cache-from: ${{ inputs.cache && 'type=gha' || '' }}
|
||||||
|
cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }}
|
||||||
|
outputs: type=image,"name=${{ env.IMAGE_NAMES }}",push-by-digest=true,name-canonical=true,push=${{ inputs.push_to_images != '' }}
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
if: ${{ inputs.push_to_images != '' }}
|
||||||
|
run: |
|
||||||
|
mkdir -p "${{ runner.temp }}/digests"
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
if: ${{ inputs.push_to_images != '' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
||||||
|
name: digests-${{ hashFiles(inputs.file_to_build) }}-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: ${{ runner.temp }}/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
# Then merge the docker images into a single one
|
||||||
|
merge-images:
|
||||||
|
if: ${{ inputs.push_to_images != '' }}
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- build-image
|
||||||
|
|
||||||
|
env:
|
||||||
|
PUSH_TO_IMAGES: ${{ inputs.push_to_images }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ${{ runner.temp }}/digests
|
||||||
|
# `hashFiles` is used to disambiguate between streaming and non-streaming images
|
||||||
|
pattern: digests-${{ hashFiles(inputs.file_to_build) }}-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
if: contains(inputs.push_to_images, 'tootsuite')
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to the GitHub Container registry
|
||||||
|
if: contains(inputs.push_to_images, 'ghcr.io')
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
if: ${{ inputs.push_to_images != '' }}
|
if: ${{ inputs.push_to_images != '' }}
|
||||||
with:
|
with:
|
||||||
images: ${{ inputs.push_to_images }}
|
images: ${{ inputs.push_to_images }}
|
||||||
@@ -85,18 +157,14 @@ jobs:
|
|||||||
tags: ${{ inputs.tags }}
|
tags: ${{ inputs.tags }}
|
||||||
labels: ${{ inputs.labels }}
|
labels: ${{ inputs.labels }}
|
||||||
|
|
||||||
- uses: docker/build-push-action@v6
|
- name: Create manifest list and push
|
||||||
with:
|
working-directory: ${{ runner.temp }}/digests
|
||||||
context: .
|
run: |
|
||||||
file: ${{ inputs.file_to_build }}
|
echo "$PUSH_TO_IMAGES" | xargs -I{} \
|
||||||
build-args: |
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
MASTODON_VERSION_PRERELEASE=${{ inputs.version_prerelease }}
|
$(printf '{}@sha256:%s ' *)
|
||||||
MASTODON_VERSION_METADATA=${{ inputs.version_metadata }}
|
|
||||||
platforms: ${{ inputs.platforms }}
|
- name: Inspect image
|
||||||
provenance: false
|
run: |
|
||||||
builder: ${{ steps.buildx.outputs.name || steps.buildx-native.outputs.name }}
|
echo "$PUSH_TO_IMAGES" | xargs -i{} \
|
||||||
push: ${{ inputs.push_to_images != '' }}
|
docker buildx imagetools inspect {}:${{ steps.meta.outputs.version }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: ${{ inputs.cache && 'type=gha' || '' }}
|
|
||||||
cache-to: ${{ inputs.cache && 'type=gha,mode=max' || '' }}
|
|
||||||
|
|||||||
4
.github/workflows/build-nightly.yml
vendored
4
.github/workflows/build-nightly.yml
vendored
@@ -26,8 +26,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
file_to_build: Dockerfile
|
file_to_build: Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
use_native_arm64_builder: false
|
|
||||||
cache: false
|
cache: false
|
||||||
push_to_images: |
|
push_to_images: |
|
||||||
ghcr.io/${{ github.repository_owner }}/mastodon
|
ghcr.io/${{ github.repository_owner }}/mastodon
|
||||||
@@ -47,8 +45,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
file_to_build: streaming/Dockerfile
|
file_to_build: streaming/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
use_native_arm64_builder: false
|
|
||||||
cache: false
|
cache: false
|
||||||
push_to_images: |
|
push_to_images: |
|
||||||
ghcr.io/${{ github.repository_owner }}/mastodon-streaming
|
ghcr.io/${{ github.repository_owner }}/mastodon-streaming
|
||||||
|
|||||||
10
.github/workflows/build-push-pr.yml
vendored
10
.github/workflows/build-push-pr.yml
vendored
@@ -21,17 +21,17 @@ 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
|
||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
file_to_build: Dockerfile
|
file_to_build: Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
use_native_arm64_builder: false
|
|
||||||
push_to_images: |
|
push_to_images: |
|
||||||
ghcr.io/${{ github.repository_owner }}/mastodon
|
ghcr.io/${{ github.repository_owner }}/mastodon
|
||||||
version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
|
version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
|
||||||
@@ -39,6 +39,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:
|
||||||
@@ -46,8 +47,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
file_to_build: streaming/Dockerfile
|
file_to_build: streaming/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
use_native_arm64_builder: false
|
|
||||||
push_to_images: |
|
push_to_images: |
|
||||||
ghcr.io/${{ github.repository_owner }}/mastodon-streaming
|
ghcr.io/${{ github.repository_owner }}/mastodon-streaming
|
||||||
version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
|
version_metadata: ${{ needs.compute-suffix.outputs.metadata }}
|
||||||
@@ -55,4 +54,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
|
||||||
|
|||||||
6
.github/workflows/build-releases.yml
vendored
6
.github/workflows/build-releases.yml
vendored
@@ -13,8 +13,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
file_to_build: Dockerfile
|
file_to_build: Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
use_native_arm64_builder: false
|
|
||||||
push_to_images: |
|
push_to_images: |
|
||||||
ghcr.io/${{ github.repository_owner }}/mastodon
|
ghcr.io/${{ github.repository_owner }}/mastodon
|
||||||
# Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages
|
# Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages
|
||||||
@@ -22,7 +20,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}}
|
||||||
@@ -33,8 +31,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
file_to_build: streaming/Dockerfile
|
file_to_build: streaming/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
use_native_arm64_builder: false
|
|
||||||
push_to_images: |
|
push_to_images: |
|
||||||
ghcr.io/${{ github.repository_owner }}/mastodon-streaming
|
ghcr.io/${{ github.repository_owner }}/mastodon-streaming
|
||||||
# Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages
|
# Do not use cache when building releases, so apt update is always ran and the release always contain the latest packages
|
||||||
|
|||||||
4
.github/workflows/build-security.yml
vendored
4
.github/workflows/build-security.yml
vendored
@@ -23,8 +23,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
file_to_build: Dockerfile
|
file_to_build: Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
use_native_arm64_builder: false
|
|
||||||
cache: false
|
cache: false
|
||||||
push_to_images: |
|
push_to_images: |
|
||||||
ghcr.io/${{ github.repository_owner }}/mastodon
|
ghcr.io/${{ github.repository_owner }}/mastodon
|
||||||
@@ -44,8 +42,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
file_to_build: streaming/Dockerfile
|
file_to_build: streaming/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
use_native_arm64_builder: false
|
|
||||||
cache: false
|
cache: false
|
||||||
push_to_images: |
|
push_to_images: |
|
||||||
ghcr.io/${{ github.repository_owner }}/mastodon-streaming
|
ghcr.io/${{ github.repository_owner }}/mastodon-streaming
|
||||||
|
|||||||
70
.github/workflows/crowdin-download-stable.yml
vendored
Normal file
70
.github/workflows/crowdin-download-stable.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
name: Crowdin / Download translations (stable branches)
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
download-translations-stable:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'glitch-soc/mastodon'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Increase Git http.postBuffer
|
||||||
|
# This is needed due to a bug in Ubuntu's cURL version?
|
||||||
|
# See https://github.com/orgs/community/discussions/55820
|
||||||
|
run: |
|
||||||
|
git config --global http.version HTTP/1.1
|
||||||
|
git config --global http.postBuffer 157286400
|
||||||
|
|
||||||
|
# Download the translation files from Crowdin
|
||||||
|
- name: crowdin action
|
||||||
|
uses: crowdin/github-action@v2
|
||||||
|
with:
|
||||||
|
config: crowdin-glitch.yml
|
||||||
|
upload_sources: false
|
||||||
|
upload_translations: false
|
||||||
|
download_translations: true
|
||||||
|
crowdin_branch_name: ${{ github.base_ref || github.ref_name }}
|
||||||
|
push_translations: false
|
||||||
|
create_pull_request: false
|
||||||
|
env:
|
||||||
|
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
|
||||||
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
|
|
||||||
|
# As the files are extracted from a Docker container, they belong to root:root
|
||||||
|
# We need to fix this before the next steps
|
||||||
|
- name: Fix file permissions
|
||||||
|
run: sudo chown -R runner:docker .
|
||||||
|
|
||||||
|
# This is needed to run the normalize step
|
||||||
|
- name: Set up Ruby environment
|
||||||
|
uses: ./.github/actions/setup-ruby
|
||||||
|
|
||||||
|
- name: Run i18n normalize task
|
||||||
|
run: bundle exec i18n-tasks normalize
|
||||||
|
|
||||||
|
# Create or update the pull request
|
||||||
|
- name: Create Pull Request
|
||||||
|
uses: peter-evans/create-pull-request@v7.0.5
|
||||||
|
with:
|
||||||
|
commit-message: 'New Crowdin translations'
|
||||||
|
title: 'New Crowdin Translations for ${{ github.base_ref || github.ref_name }} (automated)'
|
||||||
|
author: 'GitHub Actions <noreply@github.com>'
|
||||||
|
body: |
|
||||||
|
New Crowdin translations, automated with GitHub Actions
|
||||||
|
|
||||||
|
See `.github/workflows/crowdin-download.yml`
|
||||||
|
|
||||||
|
This PR will be updated every day with new translations.
|
||||||
|
|
||||||
|
Due to a limitation in GitHub Actions, checks are not running on this PR without manual action.
|
||||||
|
If you want to run the checks, then close and re-open it.
|
||||||
|
branch: i18n/crowdin/translations-${{ github.base_ref || github.ref_name }}
|
||||||
|
base: ${{ github.base_ref || github.ref_name }}
|
||||||
|
labels: i18n
|
||||||
2
.github/workflows/crowdin-download.yml
vendored
2
.github/workflows/crowdin-download.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
# Create or update the pull request
|
# Create or update the pull request
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v7.0.1
|
uses: peter-evans/create-pull-request@v7.0.5
|
||||||
with:
|
with:
|
||||||
commit-message: 'New Crowdin translations'
|
commit-message: 'New Crowdin translations'
|
||||||
title: 'New Crowdin Translations (automated)'
|
title: 'New Crowdin Translations (automated)'
|
||||||
|
|||||||
3
.github/workflows/crowdin-upload.yml
vendored
3
.github/workflows/crowdin-upload.yml
vendored
@@ -1,7 +1,6 @@
|
|||||||
name: Crowdin / Upload translations
|
name: Crowdin / Upload translations
|
||||||
|
|
||||||
on:
|
on:
|
||||||
merge_group:
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
@@ -32,7 +31,7 @@ jobs:
|
|||||||
upload_sources: true
|
upload_sources: true
|
||||||
upload_translations: false
|
upload_translations: false
|
||||||
download_translations: false
|
download_translations: false
|
||||||
crowdin_branch_name: main
|
crowdin_branch_name: ${{ github.base_ref || github.ref_name }}
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
|
CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID }}
|
||||||
|
|||||||
2
.github/workflows/test-image-build.yml
vendored
2
.github/workflows/test-image-build.yml
vendored
@@ -20,7 +20,6 @@ jobs:
|
|||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
file_to_build: Dockerfile
|
file_to_build: Dockerfile
|
||||||
platforms: linux/amd64 # Testing only on native platform so it is performant
|
|
||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
build-image-streaming:
|
build-image-streaming:
|
||||||
@@ -31,5 +30,4 @@ jobs:
|
|||||||
uses: ./.github/workflows/build-container-image.yml
|
uses: ./.github/workflows/build-container-image.yml
|
||||||
with:
|
with:
|
||||||
file_to_build: streaming/Dockerfile
|
file_to_build: streaming/Dockerfile
|
||||||
platforms: linux/amd64 # Testing only on native platform so it is performant
|
|
||||||
cache: true
|
cache: true
|
||||||
|
|||||||
2
.github/workflows/test-migrations.yml
vendored
2
.github/workflows/test-migrations.yml
vendored
@@ -32,6 +32,8 @@ jobs:
|
|||||||
postgres:
|
postgres:
|
||||||
- 14-alpine
|
- 14-alpine
|
||||||
- 15-alpine
|
- 15-alpine
|
||||||
|
- 16-alpine
|
||||||
|
- 17-alpine
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
|
|||||||
12
.github/workflows/test-ruby.yml
vendored
12
.github/workflows/test-ruby.yml
vendored
@@ -127,6 +127,7 @@ jobs:
|
|||||||
- '3.1'
|
- '3.1'
|
||||||
- '3.2'
|
- '3.2'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
|
- '3.4'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -143,7 +144,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: |
|
||||||
@@ -229,6 +230,7 @@ jobs:
|
|||||||
- '3.1'
|
- '3.1'
|
||||||
- '3.2'
|
- '3.2'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
|
- '3.4'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
@@ -245,7 +247,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'
|
||||||
@@ -308,6 +310,7 @@ jobs:
|
|||||||
- '3.1'
|
- '3.1'
|
||||||
- '3.2'
|
- '3.2'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
|
- '3.4'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -325,7 +328,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
|
||||||
@@ -425,6 +428,7 @@ jobs:
|
|||||||
- '3.1'
|
- '3.1'
|
||||||
- '3.2'
|
- '3.2'
|
||||||
- '.ruby-version'
|
- '.ruby-version'
|
||||||
|
- '3.4'
|
||||||
search-image:
|
search-image:
|
||||||
- docker.elastic.co/elasticsearch/elasticsearch:7.17.13
|
- docker.elastic.co/elasticsearch/elasticsearch:7.17.13
|
||||||
include:
|
include:
|
||||||
@@ -445,7 +449,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
|
||||||
|
|||||||
180
CHANGELOG.md
180
CHANGELOG.md
@@ -2,7 +2,157 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## [4.3.0] - UNRELEASED
|
## [4.3.5] - 2025-03-10
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change hashtag suggestion to prefer personal history capitalization (#34070 by @ClearlyClaire)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix processing errors for some HEIF images from iOS 18 (#34086 by @renchap)
|
||||||
|
- Fix streaming server not filtering unknown-language posts from public timelines (#33774 by @ClearlyClaire)
|
||||||
|
- Fix preview cards under Content Warnings not being shown in detailed statuses (#34068 by @ClearlyClaire)
|
||||||
|
- Fix username and display name being hidden on narrow screens in moderation interface (#33064 by @ClearlyClaire)
|
||||||
|
|
||||||
|
## [4.3.4] - 2025-02-27
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Update dependencies
|
||||||
|
- Change HTML sanitization to remove unusable and unused `embed` tag (#34021 by @ClearlyClaire, [GHSA-mq2m-hr29-8gqf](https://github.com/mastodon/mastodon/security/advisories/GHSA-mq2m-hr29-8gqf))
|
||||||
|
- Fix rate-limit on sign-up email verification ([GHSA-v39f-c9jj-8w7h](https://github.com/mastodon/mastodon/security/advisories/GHSA-v39f-c9jj-8w7h))
|
||||||
|
- Fix improper disclosure of domain blocks to unverified users ([GHSA-94h4-fj37-c825](https://github.com/mastodon/mastodon/security/advisories/GHSA-94h4-fj37-c825))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change preview cards to be shown when Content Warnings are expanded (#33827 by @ClearlyClaire)
|
||||||
|
- Change warnings against changing encryption secrets to be even more noticeable (#33631 by @ClearlyClaire)
|
||||||
|
- Change `mastodon:setup` to prevent overwriting already-configured servers (#33603, #33616, and #33684 by @ClearlyClaire and @mjankowski)
|
||||||
|
- Change notifications from moderators to not be filtered (#32974 and #33654 by @ClearlyClaire and @mjankowski)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix `GET /api/v2/notifications/:id` and `POST /api/v2/notifications/:id/dismiss` for ungrouped notifications (#33990 by @ClearlyClaire)
|
||||||
|
- Fix issue with some versions of libvips on some systems (#33853 by @kleisauke)
|
||||||
|
- Fix handling of duplicate mentions in incoming status `Update` (#33911 by @ClearlyClaire)
|
||||||
|
- Fix inefficiencies in timeline generation (#33839 and #33842 by @ClearlyClaire)
|
||||||
|
- Fix emoji rewrite adding unnecessary curft to the DOM for most emoji (#33818 by @ClearlyClaire)
|
||||||
|
- Fix `tootctl feeds build` not building list timelines (#33783 by @ClearlyClaire)
|
||||||
|
- Fix flaky test in `/api/v2/notifications` tests (#33773 by @ClearlyClaire)
|
||||||
|
- Fix incorrect signature after HTTP redirect (#33757 and #33769 by @ClearlyClaire)
|
||||||
|
- Fix polls not being validated on edition (#33755 by @ClearlyClaire)
|
||||||
|
- Fix media preview height in compose form when 3 or more images are attached (#33571 by @ClearlyClaire)
|
||||||
|
- Fix preview card sizing in “Author attribution” in profile settings (#33482 by @ClearlyClaire)
|
||||||
|
- Fix processing of incoming notifications for unfilterable types (#33429 by @ClearlyClaire)
|
||||||
|
- Fix featured tags for remote accounts not being kept up to date (#33372, #33406, and #33425 by @ClearlyClaire and @mjankowski)
|
||||||
|
- Fix notification polling showing a loading bar in web UI (#32960 by @Gargron)
|
||||||
|
- Fix accounts table long display name (#29316 by @WebCoder49)
|
||||||
|
- Fix exclusive lists interfering with notifications (#28162 by @ShadowJonathan)
|
||||||
|
|
||||||
|
## [4.3.3] - 2025-01-16
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix insufficient validation of account URIs ([GHSA-5wxh-3p65-r4g6](https://github.com/mastodon/mastodon/security/advisories/GHSA-5wxh-3p65-r4g6))
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix `libyaml` missing from `Dockerfile` build stage (#33591 by @vmstan)
|
||||||
|
- Fix incorrect notification settings migration for non-followers (#33348 by @ClearlyClaire)
|
||||||
|
- Fix down clause for notification policy v2 migrations (#33340 by @jesseplusplus)
|
||||||
|
- Fix error decrementing status count when `FeaturedTags#last_status_at` is `nil` (#33320 by @ClearlyClaire)
|
||||||
|
- Fix last paginated notification group only including data on a single notification (#33271 by @ClearlyClaire)
|
||||||
|
- Fix processing of mentions for post edits with an existing corresponding silent mention (#33227 by @ClearlyClaire)
|
||||||
|
- Fix deletion of unconfirmed users with Webauthn set (#33186 by @ClearlyClaire)
|
||||||
|
- Fix empty authors preview card serialization (#33151, #33466 by @mjankowski and @ClearlyClaire)
|
||||||
|
|
||||||
|
## [4.3.2] - 2024-12-03
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add `tootctl feeds vacuum` (#33065 by @ClearlyClaire)
|
||||||
|
- Add error message when user tries to follow their own account (#31910 by @lenikadali)
|
||||||
|
- Add client_secret_expires_at to OAuth Applications (#30317 by @ThisIsMissEm)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change design of Content Warnings and filters (#32543 by @ClearlyClaire)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix processing incoming post edits with mentions to unresolvable accounts (#33129 by @ClearlyClaire)
|
||||||
|
- Fix error when including multiple instances of `embed.js` (#33107 by @YKWeyer)
|
||||||
|
- Fix inactive users' timelines being backfilled on follow and unsuspend (#33094 by @ClearlyClaire)
|
||||||
|
- Fix direct inbox delivery pushing posts into inactive followers' timelines (#33067 by @ClearlyClaire)
|
||||||
|
- Fix `TagFollow` records not being correctly handled in account operations (#33063 by @ClearlyClaire)
|
||||||
|
- Fix pushing hashtag-followed posts to feeds of inactive users (#33018 by @Gargron)
|
||||||
|
- Fix duplicate notifications in notification groups when using slow mode (#33014 by @ClearlyClaire)
|
||||||
|
- Fix posts made in the future being allowed to trend (#32996 by @ClearlyClaire)
|
||||||
|
- Fix uploading higher-than-wide GIF profile picture with libvips enabled (#32911 by @ClearlyClaire)
|
||||||
|
- Fix domain attribution field having autocorrect and autocapitalize enabled (#32903 by @ClearlyClaire)
|
||||||
|
- Fix titles being escaped twice (#32889 by @ClearlyClaire)
|
||||||
|
- Fix list creation limit check (#32869 by @ClearlyClaire)
|
||||||
|
- Fix error in `tootctl email_domain_blocks` when supplying `--with-dns-records` (#32863 by @mjankowski)
|
||||||
|
- Fix `min_id` and `max_id` causing error in search API (#32857 by @Gargron)
|
||||||
|
- Fix inefficiencies when processing removal of posts that use featured tags (#32787 by @ClearlyClaire)
|
||||||
|
- Fix alt-text pop-in not using the translated description (#32766 by @ClearlyClaire)
|
||||||
|
- Fix preview cards with long titles erroneously causing layout changes (#32678 by @ClearlyClaire)
|
||||||
|
- Fix embed modal layout on mobile (#32641 by @DismalShadowX)
|
||||||
|
- Fix and improve batch attachment deletion handling when using OpenStack Swift (#32637 by @hugogameiro)
|
||||||
|
- Fix blocks not being applied on link timeline (#32625 by @tribela)
|
||||||
|
- Fix follow counters being incorrectly changed (#32622 by @oneiros)
|
||||||
|
- Fix 'unknown' media attachment type rendering (#32613 and #32713 by @ThisIsMissEm and @renatolond)
|
||||||
|
- Fix tl language native name (#32606 by @seav)
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
## [4.3.1] - 2024-10-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Add more explicit explanations about author attribution and `fediverse:creator` (#32383 by @ClearlyClaire)
|
||||||
|
- Add ability to group follow notifications in WebUI, can be disabled in the column settings (#32520 by @renchap)
|
||||||
|
- Add back a 6 hours mute duration option (#32522 by @renchap)
|
||||||
|
- Add note about not changing ActiveRecord encryption secrets once they are set (#32413, #32476, #32512, and #32537 by @ClearlyClaire and @mjankowski)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change translation feature to translate to selected regional variant (e.g. pt-BR) if available (#32428 by @c960657)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- Remove ability to get embed code for remote posts (#32578 by @ClearlyClaire)\
|
||||||
|
Getting the embed code is only reliable for local posts.\
|
||||||
|
It never worked for non-Mastodon servers, and stopped working correctly with the changes made in 4.3.0.\
|
||||||
|
We have therefore decided to remove the menu entry while we investigate solutions.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix follow recommendation moderation page default language when using regional variant (#32580 by @ClearlyClaire)
|
||||||
|
- Fix column-settings spacing in local timeline in advanced view (#32567 by @lindwurm)
|
||||||
|
- Fix broken i18n in text welcome mailer tags area (#32571 by @mjankowski)
|
||||||
|
- Fix missing or incorrect cache-control headers for Streaming server (#32551 by @ThisIsMissEm)
|
||||||
|
- Fix only the first paragraph being displayed in some notifications (#32348 by @ClearlyClaire)
|
||||||
|
- Fix reblog icons on account media view (#32506 by @tribela)
|
||||||
|
- Fix Content-Security-Policy not allowing OpenStack SWIFT object storage URI (#32439 by @kenkiku1021)
|
||||||
|
- Fix back arrow pointing to the incorrect direction in RTL languages (#32485 by @renchap)
|
||||||
|
- Fix streaming server using `REDIS_USERNAME` instead of `REDIS_USER` (#32493 by @ThisIsMissEm)
|
||||||
|
- Fix follow recommendation carrousel scrolling on RTL layouts (#32462 and #32505 by @ClearlyClaire)
|
||||||
|
- Fix follow recommendation suppressions not applying immediately (#32392 by @ClearlyClaire)
|
||||||
|
- Fix language of push notifications (#32415 by @ClearlyClaire)
|
||||||
|
- Fix mute duration not being shown in list of muted accounts in web UI (#32388 by @ClearlyClaire)
|
||||||
|
- Fix “Mark every notification as read” not updating the read marker if scrolled down (#32385 by @ClearlyClaire)
|
||||||
|
- Fix “Mention” appearing for otherwise filtered posts (#32356 by @ClearlyClaire)
|
||||||
|
- Fix notification requests from suspended accounts still being listed (#32354 by @ClearlyClaire)
|
||||||
|
- Fix list edition modal styling (#32358 and #32367 by @ClearlyClaire and @vmstan)
|
||||||
|
- Fix 4 columns barely not fitting on 1920px screen (#32361 by @ClearlyClaire)
|
||||||
|
- Fix icon alignment in applications list (#32293 by @mjankowski)
|
||||||
|
|
||||||
|
## [4.3.0] - 2024-10-08
|
||||||
|
|
||||||
The following changelog entries focus on changes visible to users, administrators, client developers or federated software developers, but there has also been a lot of code modernization, refactoring, and tooling work, in particular by @mjankowski.
|
The following changelog entries focus on changes visible to users, administrators, client developers or federated software developers, but there has also been a lot of code modernization, refactoring, and tooling work, in particular by @mjankowski.
|
||||||
|
|
||||||
@@ -11,12 +161,12 @@ The following changelog entries focus on changes visible to users, administrator
|
|||||||
- **Add confirmation interstitial instead of silently redirecting logged-out visitors to remote resources** (#27792, #28902, and #30651 by @ClearlyClaire and @Gargron)\
|
- **Add confirmation interstitial instead of silently redirecting logged-out visitors to remote resources** (#27792, #28902, and #30651 by @ClearlyClaire and @Gargron)\
|
||||||
This fixes a longstanding open redirect in Mastodon, at the cost of added friction when local links to remote resources are shared.
|
This fixes a longstanding open redirect in Mastodon, at the cost of added friction when local links to remote resources are shared.
|
||||||
- Fix ReDoS vulnerability on some Ruby versions ([GHSA-jpxp-r43f-rhvx](https://github.com/mastodon/mastodon/security/advisories/GHSA-jpxp-r43f-rhvx))
|
- Fix ReDoS vulnerability on some Ruby versions ([GHSA-jpxp-r43f-rhvx](https://github.com/mastodon/mastodon/security/advisories/GHSA-jpxp-r43f-rhvx))
|
||||||
- Change `form-action` Content-Security-Policy directive to be more restrictive (#26897 by @ClearlyClaire)
|
- Change `form-action` Content-Security-Policy directive to be more restrictive (#26897 and #32241 by @ClearlyClaire)
|
||||||
- Update dependencies
|
- Update dependencies
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- **Add server-side notification grouping** (#29889, #30576, #30685, #30688, #30707, #30776, #30779, #30781, #30440, #31062, #31098, #31076, #31111, #31123, #31223, #31214, #31224, #31299, #31325, #31347, #31304, #31326, #31384, #31403, #31433, #31509, #31486, #31513, #31592, #31594, #31638, #31746, #31652, #31709, #31725, #31745, #31613, #31657, #31840, #31610, #31929, #32089 and #32085 by @ClearlyClaire, @Gargron, @mgmn, and @renchap)\
|
- **Add server-side notification grouping** (#29889, #30576, #30685, #30688, #30707, #30776, #30779, #30781, #30440, #31062, #31098, #31076, #31111, #31123, #31223, #31214, #31224, #31299, #31325, #31347, #31304, #31326, #31384, #31403, #31433, #31509, #31486, #31513, #31592, #31594, #31638, #31746, #31652, #31709, #31725, #31745, #31613, #31657, #31840, #31610, #31929, #32089, #32085, #32243, #32179 and #32254 by @ClearlyClaire, @Gargron, @mgmn, and @renchap)\
|
||||||
Group notifications of the same type for the same target, so that your notifications no longer get cluttered by boost and favorite notifications as soon as a couple of your posts get traction.\
|
Group notifications of the same type for the same target, so that your notifications no longer get cluttered by boost and favorite notifications as soon as a couple of your posts get traction.\
|
||||||
This is done server-side so that clients can efficiently get relevant groups without having to go through numerous pages of individual notifications.\
|
This is done server-side so that clients can efficiently get relevant groups without having to go through numerous pages of individual notifications.\
|
||||||
As part of this, the visual design of the entire notifications feature has been revamped.\
|
As part of this, the visual design of the entire notifications feature has been revamped.\
|
||||||
@@ -28,7 +178,7 @@ The following changelog entries focus on changes visible to users, administrator
|
|||||||
- `GET /api/v2/notifications/:group_key/accounts`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts
|
- `GET /api/v2/notifications/:group_key/accounts`: https://docs.joinmastodon.org/methods/grouped_notifications/#get-group-accounts
|
||||||
- `POST /api/v2/notifications/:group_key/dimsiss`: https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group
|
- `POST /api/v2/notifications/:group_key/dimsiss`: https://docs.joinmastodon.org/methods/grouped_notifications/#dismiss-group
|
||||||
- `GET /api/v2/notifications/:unread_count`: https://docs.joinmastodon.org/methods/grouped_notifications/#unread-group-count
|
- `GET /api/v2/notifications/:unread_count`: https://docs.joinmastodon.org/methods/grouped_notifications/#unread-group-count
|
||||||
- **Add notification policies, filtered notifications and notification requests** (#29366, #29529, #29433, #29565, #29567, #29572, #29575, #29588, #29646, #29652, #29658, #29666, #29693, #29699, #29737, #29706, #29570, #29752, #29810, #29826, #30114, #30251, #30559, #29868, #31008, #31011, #30996, #31149, #31220, #31222, #31225, #31242, #31262, #31250, #31273, #31310, #31316, #31322, #31329, #31324, #31331, #31343, #31342, #31309, #31358, #31378, #31406, #31256, #31456, #31419, #31457, #31508, #31540, #31541, #31723 and #32062 by @ClearlyClaire, @Gargron, @TheEssem, @mgmn, @oneiros, and @renchap)\
|
- **Add notification policies, filtered notifications and notification requests** (#29366, #29529, #29433, #29565, #29567, #29572, #29575, #29588, #29646, #29652, #29658, #29666, #29693, #29699, #29737, #29706, #29570, #29752, #29810, #29826, #30114, #30251, #30559, #29868, #31008, #31011, #30996, #31149, #31220, #31222, #31225, #31242, #31262, #31250, #31273, #31310, #31316, #31322, #31329, #31324, #31331, #31343, #31342, #31309, #31358, #31378, #31406, #31256, #31456, #31419, #31457, #31508, #31540, #31541, #31723, #32062 and #32281 by @ClearlyClaire, @Gargron, @TheEssem, @mgmn, @oneiros, and @renchap)\
|
||||||
The old “Block notifications from non-followers”, “Block notifications from people you don't follow” and “Block direct messages from people you don't follow” notification settings have been replaced by a new set of settings found directly in the notification column.\
|
The old “Block notifications from non-followers”, “Block notifications from people you don't follow” and “Block direct messages from people you don't follow” notification settings have been replaced by a new set of settings found directly in the notification column.\
|
||||||
You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\
|
You can now separately filter or drop notifications from people you don't follow, people who don't follow you, accounts created within the past 30 days, as well as unsolicited private mentions, and accounts limited by the moderation.\
|
||||||
Instead of being outright dropped, notifications that you chose to filter are put in a separate “Filtered notifications” box that you can review separately without it clogging your main notifications.\
|
Instead of being outright dropped, notifications that you chose to filter are put in a separate “Filtered notifications” box that you can review separately without it clogging your main notifications.\
|
||||||
@@ -51,7 +201,7 @@ The following changelog entries focus on changes visible to users, administrator
|
|||||||
- **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\
|
- **Add notifications of severed relationships** (#27511, #29665, #29668, #29670, #29700, #29714, #29712, and #29731 by @ClearlyClaire and @Gargron)\
|
||||||
Notify local users when they lose relationships as a result of a local moderator blocking a remote account or server, allowing the affected user to retrieve the list of broken relationships.\
|
Notify local users when they lose relationships as a result of a local moderator blocking a remote account or server, allowing the affected user to retrieve the list of broken relationships.\
|
||||||
Note that this does not notify remote users.\
|
Note that this does not notify remote users.\
|
||||||
This adds the `severed_relationships` notification type to the REST API and streaming, with a new [`relationship_severance_event` attribute](https://docs.joinmastodon.org/entities/Notification/#relationship_severance_event).
|
This adds the `severed_relationships` notification type to the REST API and streaming, with a new [`event` attribute](https://docs.joinmastodon.org/entities/Notification/#relationship_severance_event).
|
||||||
- **Add hover cards in web UI** (#30754, #30864, #30850, #30879, #30928, #30949, #30948, #30931, and #31300 by @ClearlyClaire, @Gargron, and @renchap)\
|
- **Add hover cards in web UI** (#30754, #30864, #30850, #30879, #30928, #30949, #30948, #30931, and #31300 by @ClearlyClaire, @Gargron, and @renchap)\
|
||||||
Hovering over an avatar or username will now display a hover card with the first two lines of the user's description and their first two profile fields.\
|
Hovering over an avatar or username will now display a hover card with the first two lines of the user's description and their first two profile fields.\
|
||||||
This can be disabled in the “Animations and accessibility” section of the preferences.
|
This can be disabled in the “Animations and accessibility” section of the preferences.
|
||||||
@@ -61,13 +211,13 @@ The following changelog entries focus on changes visible to users, administrator
|
|||||||
- **Add timeline of public posts about a trending link** (#30381 and #30840 by @Gargron)\
|
- **Add timeline of public posts about a trending link** (#30381 and #30840 by @Gargron)\
|
||||||
You can now see public posts mentioning currently-trending articles from people who have opted into discovery features.\
|
You can now see public posts mentioning currently-trending articles from people who have opted into discovery features.\
|
||||||
This adds a new REST API endpoint: https://docs.joinmastodon.org/methods/timelines/#link
|
This adds a new REST API endpoint: https://docs.joinmastodon.org/methods/timelines/#link
|
||||||
- **Add author highlight for news articles whose authors are on the fediverse** (#30398, #30670, #30521, #30846, #31819, and #31900 by @Gargron and @oneiros)\
|
- **Add author highlight for news articles whose authors are on the fediverse** (#30398, #30670, #30521, #30846, #31819, #31900 and #32188 by @Gargron, @mjankowski and @oneiros)\
|
||||||
This adds a mechanism to [highlight the author of news articles](https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/) shared on Mastodon.\
|
This adds a mechanism to [highlight the author of news articles](https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/) shared on Mastodon.\
|
||||||
Articles hosted outside the fediverse can indicate a fediverse author with a meta tag:
|
Articles hosted outside the fediverse can indicate a fediverse author with a meta tag:
|
||||||
```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)\
|
||||||
@@ -150,10 +300,12 @@ The following changelog entries focus on changes visible to users, administrator
|
|||||||
- Add groundwork for annual reports for accounts (#28693 by @Gargron)\
|
- Add groundwork for annual reports for accounts (#28693 by @Gargron)\
|
||||||
This lays the groundwork for a “year-in-review”/“wrapped” style report for local users, but is currently not in use.
|
This lays the groundwork for a “year-in-review”/“wrapped” style report for local users, but is currently not in use.
|
||||||
- Add notification email on invalid second authenticator (#28822 by @ClearlyClaire)
|
- Add notification email on invalid second authenticator (#28822 by @ClearlyClaire)
|
||||||
|
- Add date of account deletion in list of accounts in the admin interface (#25640 by @tribela)
|
||||||
- Add new emojis from `jdecked/twemoji` 15.0 (#28404 by @TheEssem)
|
- Add new emojis from `jdecked/twemoji` 15.0 (#28404 by @TheEssem)
|
||||||
- Add configurable error handling in attachment batch deletion (#28184 by @vmstan)\
|
- Add configurable error handling in attachment batch deletion (#28184 by @vmstan)\
|
||||||
This makes the S3 batch size configurable through the `S3_BATCH_DELETE_LIMIT` environment variable (defaults to 1000), and adds some retry logic, configurable through the `S3_BATCH_DELETE_RETRY` environment variable (defaults to 3).
|
This makes the S3 batch size configurable through the `S3_BATCH_DELETE_LIMIT` environment variable (defaults to 1000), and adds some retry logic, configurable through the `S3_BATCH_DELETE_RETRY` environment variable (defaults to 3).
|
||||||
- Add VAPID public key to instance serializer (#28006 by @ThisIsMissEm)
|
- Add VAPID public key to instance serializer (#28006 by @ThisIsMissEm)
|
||||||
|
- Add support for serving JRD `/.well-known/host-meta.json` in addition to XRD host-meta (#32206 by @c960657)
|
||||||
- Add `nodeName` and `nodeDescription` to nodeinfo `metadata` (#28079 by @6543)
|
- Add `nodeName` and `nodeDescription` to nodeinfo `metadata` (#28079 by @6543)
|
||||||
- Add Thai diacritics and tone marks in `HASHTAG_INVALID_CHARS_RE` (#26576 by @ppnplus)
|
- Add Thai diacritics and tone marks in `HASHTAG_INVALID_CHARS_RE` (#26576 by @ppnplus)
|
||||||
- Add variable delay before link verification of remote account links (#27774 by @ClearlyClaire)
|
- Add variable delay before link verification of remote account links (#27774 by @ClearlyClaire)
|
||||||
@@ -168,7 +320,7 @@ The following changelog entries focus on changes visible to users, administrator
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- **Change icons throughout the web interface** (#27385, #27539, #27555, #27579, #27700, #27817, #28519, #28709, #28064, #28775, #28780, #27924, #29294, #29395, #29537, #29569, #29610, #29612, #29649, #29844, #27780, #30974, #30963, #30962, #30961, #31362, #31363, #31359, #31371, #31360, #31512, #31511, and #31525 by @ClearlyClaire, @Gargron, @arbolitoloco1, @mjankowski, @nclm, @renchap, @ronilaukkarinen, and @zunda)\
|
- **Change icons throughout the web interface** (#27385, #27539, #27555, #27579, #27700, #27817, #28519, #28709, #28064, #28775, #28780, #27924, #29294, #29395, #29537, #29569, #29610, #29612, #29649, #29844, #27780, #30974, #30963, #30962, #30961, #31362, #31363, #31359, #31371, #31360, #31512, #31511, #31525, #32153, and #32201 by @ClearlyClaire, @Gargron, @arbolitoloco1, @mjankowski, @nclm, @renchap, @ronilaukkarinen, and @zunda)\
|
||||||
This changes all the interface icons from FontAwesome to Material Symbols for a more modern look, consistent with the official Mastodon Android app.\
|
This changes all the interface icons from FontAwesome to Material Symbols for a more modern look, consistent with the official Mastodon Android app.\
|
||||||
In addition, better care is given to pixel alignment, and icon variants are used to better highlight active/inactive state.
|
In addition, better care is given to pixel alignment, and icon variants are used to better highlight active/inactive state.
|
||||||
- **Change design of compose form in web UI** (#28119, #29059, #29248, #29372, #29384, #29417, #29456, #29406, #29651, #29659, #31889 and #32033 by @ClearlyClaire, @Gargron, @eai04191, @hinaloe, and @ronilaukkarinen)\
|
- **Change design of compose form in web UI** (#28119, #29059, #29248, #29372, #29384, #29417, #29456, #29406, #29651, #29659, #31889 and #32033 by @ClearlyClaire, @Gargron, @eai04191, @hinaloe, and @ronilaukkarinen)\
|
||||||
@@ -192,9 +344,9 @@ The following changelog entries focus on changes visible to users, administrator
|
|||||||
Administrators may need to update their setup accordingly.
|
Administrators may need to update their setup accordingly.
|
||||||
- Change how content warnings and filters are displayed in web UI (#31365, and #31761 by @Gargron)
|
- Change how content warnings and filters are displayed in web UI (#31365, and #31761 by @Gargron)
|
||||||
- Change preview card processing to ignore `undefined` as canonical url (#31882 by @oneiros)
|
- Change preview card processing to ignore `undefined` as canonical url (#31882 by @oneiros)
|
||||||
- Change embedded posts to use web UI (#31766 and #32135 by @Gargron)
|
- Change embedded posts to use web UI (#31766, #32135 and #32271 by @Gargron)
|
||||||
- Change inner borders in media galleries in web UI (#31852 by @Gargron)
|
- Change inner borders in media galleries in web UI (#31852 by @Gargron)
|
||||||
- Change design of media attachments and profile media tab in web UI (#31807, #32048, and #31967 by @Gargron)
|
- Change design of media attachments and profile media tab in web UI (#31807, #32048, #31967, #32217, #32224 and #32257 by @ClearlyClaire and @Gargron)
|
||||||
- Change labels on thread indicators in web UI (#31806 by @Gargron)
|
- Change labels on thread indicators in web UI (#31806 by @Gargron)
|
||||||
- Change label of "Data export" menu item in settings interface (#32099 by @c960657)
|
- Change label of "Data export" menu item in settings interface (#32099 by @c960657)
|
||||||
- Change responsive break points on navigation panel in web UI (#32034 by @Gargron)
|
- Change responsive break points on navigation panel in web UI (#32034 by @Gargron)
|
||||||
@@ -284,9 +436,10 @@ The following changelog entries focus on changes visible to users, administrator
|
|||||||
- Fix error when accepting an appeal for sensitive posts deleted in the meantime (#32037 by @ClearlyClaire)
|
- Fix error when accepting an appeal for sensitive posts deleted in the meantime (#32037 by @ClearlyClaire)
|
||||||
- Fix error when encountering reblog of deleted post in feed rebuild (#32001 by @ClearlyClaire)
|
- Fix error when encountering reblog of deleted post in feed rebuild (#32001 by @ClearlyClaire)
|
||||||
- Fix Safari browser glitch related to horizontal scrolling (#31960 by @Gargron)
|
- Fix Safari browser glitch related to horizontal scrolling (#31960 by @Gargron)
|
||||||
|
- Fix unresolvable mentions sometimes preventing processing incoming posts (#29215 by @tribela and @ClearlyClaire)
|
||||||
- Fix too many requests caused by relationship look-ups in web UI (#32042 by @Gargron)
|
- Fix too many requests caused by relationship look-ups in web UI (#32042 by @Gargron)
|
||||||
- Fix links for reblogs in moderation interface (#31979 by @ClearlyClaire)
|
- Fix links for reblogs in moderation interface (#31979 by @ClearlyClaire)
|
||||||
- Fix the appearance of avatars when they do not load (#31966 by @renchap)
|
- Fix the appearance of avatars when they do not load (#31966 and #32270 by @Gargron and @renchap)
|
||||||
- Fix spurious error notifications for aborted requests in web UI (#31952 by @c960657)
|
- Fix spurious error notifications for aborted requests in web UI (#31952 by @c960657)
|
||||||
- Fix HTTP 500 error in `/api/v1/polls/:id/votes` when required `choices` parameter is missing (#25598 by @danielmbrasil)
|
- Fix HTTP 500 error in `/api/v1/polls/:id/votes` when required `choices` parameter is missing (#25598 by @danielmbrasil)
|
||||||
- Fix security context sometimes not being added in LD-Signed activities (#31871 by @ClearlyClaire)
|
- Fix security context sometimes not being added in LD-Signed activities (#31871 by @ClearlyClaire)
|
||||||
@@ -309,10 +462,12 @@ The following changelog entries focus on changes visible to users, administrator
|
|||||||
- Fix “Redirect URI” field not being marked as required in “New application” form (#30311 by @ThisIsMissEm)
|
- Fix “Redirect URI” field not being marked as required in “New application” form (#30311 by @ThisIsMissEm)
|
||||||
- Fix right-to-left text in preview cards (#30930 by @ClearlyClaire)
|
- Fix right-to-left text in preview cards (#30930 by @ClearlyClaire)
|
||||||
- Fix rack attack `match_type` value typo in logging config (#30514 by @mjankowski)
|
- Fix rack attack `match_type` value typo in logging config (#30514 by @mjankowski)
|
||||||
- Fix various cases of duplicate, missing, or inconsistent borders or scrollbar styles (#31068, #31286, #31268, #31275, #31284, #31305, #31346, #31372, #31373, #31389, #31432, #31391, #31445 and #32091 by @ClearlyClaire, @valtlai and @vmstan)
|
- Fix various cases of duplicate, missing, or inconsistent borders or scrollbar styles (#31068, #31286, #31268, #31275, #31284, #31305, #31346, #31372, #31373, #31389, #31432, #31391, #31445, #32091, #32147 and #32137 by @ClearlyClaire, @mjankowski, @valtlai and @vmstan)
|
||||||
|
- Fix editing description of media uploads with custom thumbnails (#32221 by @ClearlyClaire)
|
||||||
- Fix race condition in `POST /api/v1/push/subscription` (#30166 by @ClearlyClaire)
|
- Fix race condition in `POST /api/v1/push/subscription` (#30166 by @ClearlyClaire)
|
||||||
- Fix post deletion not being delayed when those are part of an account warning (#30163 by @ClearlyClaire)
|
- Fix post deletion not being delayed when those are part of an account warning (#30163 by @ClearlyClaire)
|
||||||
- Fix rendering error on `/start` when not logged in (#30023 by @timothyjrogers)
|
- Fix rendering error on `/start` when not logged in (#30023 by @timothyjrogers)
|
||||||
|
- Fix unneeded requests to blocked domains when receiving relayed signed activities from them (#31161 by @ClearlyClaire)
|
||||||
- Fix logo pushing header buttons out of view on certain conditions in mobile layout (#29787 by @ClearlyClaire)
|
- Fix logo pushing header buttons out of view on certain conditions in mobile layout (#29787 by @ClearlyClaire)
|
||||||
- Fix notification-related records not being reattributed when merging accounts (#29694 by @ClearlyClaire)
|
- Fix notification-related records not being reattributed when merging accounts (#29694 by @ClearlyClaire)
|
||||||
- Fix results/query in `api/v1/featured_tags/suggestions` (#29597 by @mjankowski)
|
- Fix results/query in `api/v1/featured_tags/suggestions` (#29597 by @mjankowski)
|
||||||
@@ -322,6 +477,7 @@ The following changelog entries focus on changes visible to users, administrator
|
|||||||
- Fix full date display not respecting the locale 12/24h format (#29448 by @renchap)
|
- Fix full date display not respecting the locale 12/24h format (#29448 by @renchap)
|
||||||
- Fix filters title and keywords overflow (#29396 by @GeopJr)
|
- Fix filters title and keywords overflow (#29396 by @GeopJr)
|
||||||
- Fix incorrect date format in “Follows and followers” (#29390 by @JasonPunyon)
|
- Fix incorrect date format in “Follows and followers” (#29390 by @JasonPunyon)
|
||||||
|
- Fix navigation item active highlight for some paths (#32159 by @mjankowski)
|
||||||
- Fix “Edit media” modal sizing and layout when space-constrained (#27095 by @ronilaukkarinen)
|
- Fix “Edit media” modal sizing and layout when space-constrained (#27095 by @ronilaukkarinen)
|
||||||
- Fix modal container bounds (#29185 by @nico3333fr)
|
- Fix modal container bounds (#29185 by @nico3333fr)
|
||||||
- Fix inefficient HTTP signature parsing using regexps and `StringScanner` (#29133 by @ClearlyClaire)
|
- Fix inefficient HTTP signature parsing using regexps and `StringScanner` (#29133 by @ClearlyClaire)
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ RUN \
|
|||||||
# Set /opt/mastodon as working directory
|
# Set /opt/mastodon as working directory
|
||||||
WORKDIR /opt/mastodon
|
WORKDIR /opt/mastodon
|
||||||
|
|
||||||
|
# Add backport repository for some specific packages where we need the latest version
|
||||||
|
RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list
|
||||||
|
|
||||||
# hadolint ignore=DL3008,DL3005
|
# hadolint ignore=DL3008,DL3005
|
||||||
RUN \
|
RUN \
|
||||||
# Mount Apt cache and lib directories from Docker buildx caches
|
# Mount Apt cache and lib directories from Docker buildx caches
|
||||||
@@ -150,6 +153,7 @@ RUN \
|
|||||||
libpq-dev \
|
libpq-dev \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
libtool \
|
libtool \
|
||||||
|
libyaml-dev \
|
||||||
meson \
|
meson \
|
||||||
nasm \
|
nasm \
|
||||||
pkg-config \
|
pkg-config \
|
||||||
@@ -160,7 +164,7 @@ RUN \
|
|||||||
libexif-dev \
|
libexif-dev \
|
||||||
libexpat1-dev \
|
libexpat1-dev \
|
||||||
libgirepository1.0-dev \
|
libgirepository1.0-dev \
|
||||||
libheif-dev \
|
libheif-dev/bookworm-backports \
|
||||||
libimagequant-dev \
|
libimagequant-dev \
|
||||||
libjpeg62-turbo-dev \
|
libjpeg62-turbo-dev \
|
||||||
liblcms2-dev \
|
liblcms2-dev \
|
||||||
@@ -343,7 +347,7 @@ RUN \
|
|||||||
# libvips components
|
# libvips components
|
||||||
libcgif0 \
|
libcgif0 \
|
||||||
libexif12 \
|
libexif12 \
|
||||||
libheif1 \
|
libheif1/bookworm-backports \
|
||||||
libimagequant0 \
|
libimagequant0 \
|
||||||
libjpeg62-turbo \
|
libjpeg62-turbo \
|
||||||
liblcms2-2 \
|
liblcms2-2 \
|
||||||
|
|||||||
157
Gemfile.lock
157
Gemfile.lock
@@ -10,35 +10,35 @@ GIT
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
actioncable (7.1.4)
|
actioncable (7.1.5.1)
|
||||||
actionpack (= 7.1.4)
|
actionpack (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.1.5.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.5.1)
|
||||||
actionpack (= 7.1.4)
|
actionpack (= 7.1.5.1)
|
||||||
activejob (= 7.1.4)
|
activejob (= 7.1.5.1)
|
||||||
activerecord (= 7.1.4)
|
activerecord (= 7.1.5.1)
|
||||||
activestorage (= 7.1.4)
|
activestorage (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.1.5.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.5.1)
|
||||||
actionpack (= 7.1.4)
|
actionpack (= 7.1.5.1)
|
||||||
actionview (= 7.1.4)
|
actionview (= 7.1.5.1)
|
||||||
activejob (= 7.1.4)
|
activejob (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.1.5.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.5.1)
|
||||||
actionview (= 7.1.4)
|
actionview (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.1.5.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.5.1)
|
||||||
actionpack (= 7.1.4)
|
actionpack (= 7.1.5.1)
|
||||||
activerecord (= 7.1.4)
|
activerecord (= 7.1.5.1)
|
||||||
activestorage (= 7.1.4)
|
activestorage (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.1.5.1)
|
||||||
globalid (>= 0.6.0)
|
globalid (>= 0.6.0)
|
||||||
nokogiri (>= 1.8.5)
|
nokogiri (>= 1.8.5)
|
||||||
actionview (7.1.4)
|
actionview (7.1.5.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.1.5.1)
|
||||||
builder (~> 3.1)
|
builder (~> 3.1)
|
||||||
erubi (~> 1.11)
|
erubi (~> 1.11)
|
||||||
rails-dom-testing (~> 2.2)
|
rails-dom-testing (~> 2.2)
|
||||||
@@ -64,30 +64,33 @@ 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.5.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.1.5.1)
|
||||||
globalid (>= 0.3.6)
|
globalid (>= 0.3.6)
|
||||||
activemodel (7.1.4)
|
activemodel (7.1.5.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.1.5.1)
|
||||||
activerecord (7.1.4)
|
activerecord (7.1.5.1)
|
||||||
activemodel (= 7.1.4)
|
activemodel (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.1.5.1)
|
||||||
timeout (>= 0.4.0)
|
timeout (>= 0.4.0)
|
||||||
activestorage (7.1.4)
|
activestorage (7.1.5.1)
|
||||||
actionpack (= 7.1.4)
|
actionpack (= 7.1.5.1)
|
||||||
activejob (= 7.1.4)
|
activejob (= 7.1.5.1)
|
||||||
activerecord (= 7.1.4)
|
activerecord (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.1.5.1)
|
||||||
marcel (~> 1.0)
|
marcel (~> 1.0)
|
||||||
activesupport (7.1.4)
|
activesupport (7.1.5.1)
|
||||||
base64
|
base64
|
||||||
|
benchmark (>= 0.3)
|
||||||
bigdecimal
|
bigdecimal
|
||||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||||
connection_pool (>= 2.2.5)
|
connection_pool (>= 2.2.5)
|
||||||
drb
|
drb
|
||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
|
logger (>= 1.4.2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
mutex_m
|
mutex_m
|
||||||
|
securerandom (>= 0.3)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
addressable (2.8.7)
|
addressable (2.8.7)
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
@@ -126,6 +129,7 @@ GEM
|
|||||||
base64 (0.2.0)
|
base64 (0.2.0)
|
||||||
bcp47_spec (0.2.1)
|
bcp47_spec (0.2.1)
|
||||||
bcrypt (3.1.20)
|
bcrypt (3.1.20)
|
||||||
|
benchmark (0.4.0)
|
||||||
better_errors (2.10.1)
|
better_errors (2.10.1)
|
||||||
erubi (>= 1.0.0)
|
erubi (>= 1.0.0)
|
||||||
rack (>= 0.9.0)
|
rack (>= 0.9.0)
|
||||||
@@ -262,15 +266,15 @@ GEM
|
|||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fast_blank (1.0.1)
|
fast_blank (1.0.1)
|
||||||
fastimage (2.3.1)
|
fastimage (2.3.1)
|
||||||
ffi (1.16.3)
|
ffi (1.17.1)
|
||||||
ffi-compiler (1.3.2)
|
ffi-compiler (1.3.2)
|
||||||
ffi (>= 1.15.5)
|
ffi (>= 1.15.5)
|
||||||
rake
|
rake
|
||||||
flatware (2.3.3)
|
flatware (2.3.4)
|
||||||
drb
|
drb
|
||||||
thor (< 2.0)
|
thor (< 2.0)
|
||||||
flatware-rspec (2.3.3)
|
flatware-rspec (2.3.4)
|
||||||
flatware (= 2.3.3)
|
flatware (= 2.3.4)
|
||||||
rspec (>= 3.6)
|
rspec (>= 3.6)
|
||||||
fog-core (2.5.0)
|
fog-core (2.5.0)
|
||||||
builder
|
builder
|
||||||
@@ -406,7 +410,7 @@ GEM
|
|||||||
llhttp-ffi (0.5.0)
|
llhttp-ffi (0.5.0)
|
||||||
ffi-compiler (~> 1.0)
|
ffi-compiler (~> 1.0)
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
logger (1.6.1)
|
logger (1.6.6)
|
||||||
lograge (0.14.0)
|
lograge (0.14.0)
|
||||||
actionpack (>= 4)
|
actionpack (>= 4)
|
||||||
activesupport (>= 4)
|
activesupport (>= 4)
|
||||||
@@ -433,7 +437,7 @@ GEM
|
|||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
mime-types-data (3.2024.0820)
|
mime-types-data (3.2024.0820)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
mini_portile2 (2.8.7)
|
mini_portile2 (2.8.8)
|
||||||
minitest (5.25.1)
|
minitest (5.25.1)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
@@ -443,7 +447,7 @@ GEM
|
|||||||
uri
|
uri
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
net-imap (0.4.15)
|
net-imap (0.4.19)
|
||||||
date
|
date
|
||||||
net-protocol
|
net-protocol
|
||||||
net-ldap (0.19.0)
|
net-ldap (0.19.0)
|
||||||
@@ -451,10 +455,10 @@ GEM
|
|||||||
net-protocol
|
net-protocol
|
||||||
net-protocol (0.2.2)
|
net-protocol (0.2.2)
|
||||||
timeout
|
timeout
|
||||||
net-smtp (0.5.0)
|
net-smtp (0.5.1)
|
||||||
net-protocol
|
net-protocol
|
||||||
nio4r (2.7.3)
|
nio4r (2.7.3)
|
||||||
nokogiri (1.16.7)
|
nokogiri (1.18.3)
|
||||||
mini_portile2 (~> 2.8.2)
|
mini_portile2 (~> 2.8.2)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oj (3.16.6)
|
oj (3.16.6)
|
||||||
@@ -601,7 +605,7 @@ GEM
|
|||||||
actionmailer (>= 3)
|
actionmailer (>= 3)
|
||||||
net-smtp
|
net-smtp
|
||||||
premailer (~> 1.7, >= 1.7.9)
|
premailer (~> 1.7, >= 1.7.9)
|
||||||
propshaft (1.0.1)
|
propshaft (1.1.0)
|
||||||
actionpack (>= 7.0.0)
|
actionpack (>= 7.0.0)
|
||||||
activesupport (>= 7.0.0)
|
activesupport (>= 7.0.0)
|
||||||
rack
|
rack
|
||||||
@@ -615,7 +619,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.11)
|
||||||
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 +642,20 @@ GEM
|
|||||||
rackup (1.0.0)
|
rackup (1.0.0)
|
||||||
rack (< 3)
|
rack (< 3)
|
||||||
webrick
|
webrick
|
||||||
rails (7.1.4)
|
rails (7.1.5.1)
|
||||||
actioncable (= 7.1.4)
|
actioncable (= 7.1.5.1)
|
||||||
actionmailbox (= 7.1.4)
|
actionmailbox (= 7.1.5.1)
|
||||||
actionmailer (= 7.1.4)
|
actionmailer (= 7.1.5.1)
|
||||||
actionpack (= 7.1.4)
|
actionpack (= 7.1.5.1)
|
||||||
actiontext (= 7.1.4)
|
actiontext (= 7.1.5.1)
|
||||||
actionview (= 7.1.4)
|
actionview (= 7.1.5.1)
|
||||||
activejob (= 7.1.4)
|
activejob (= 7.1.5.1)
|
||||||
activemodel (= 7.1.4)
|
activemodel (= 7.1.5.1)
|
||||||
activerecord (= 7.1.4)
|
activerecord (= 7.1.5.1)
|
||||||
activestorage (= 7.1.4)
|
activestorage (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.1.5.1)
|
||||||
bundler (>= 1.15.0)
|
bundler (>= 1.15.0)
|
||||||
railties (= 7.1.4)
|
railties (= 7.1.5.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)
|
||||||
@@ -660,15 +664,15 @@ GEM
|
|||||||
activesupport (>= 5.0.0)
|
activesupport (>= 5.0.0)
|
||||||
minitest
|
minitest
|
||||||
nokogiri (>= 1.6)
|
nokogiri (>= 1.6)
|
||||||
rails-html-sanitizer (1.6.0)
|
rails-html-sanitizer (1.6.2)
|
||||||
loofah (~> 2.21)
|
loofah (~> 2.21)
|
||||||
nokogiri (~> 1.14)
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
||||||
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.5.1)
|
||||||
actionpack (= 7.1.4)
|
actionpack (= 7.1.5.1)
|
||||||
activesupport (= 7.1.4)
|
activesupport (= 7.1.5.1)
|
||||||
irb
|
irb
|
||||||
rackup (>= 1.0.0)
|
rackup (>= 1.0.0)
|
||||||
rake (>= 12.2)
|
rake (>= 12.2)
|
||||||
@@ -698,7 +702,7 @@ GEM
|
|||||||
responders (3.1.1)
|
responders (3.1.1)
|
||||||
actionpack (>= 5.2)
|
actionpack (>= 5.2)
|
||||||
railties (>= 5.2)
|
railties (>= 5.2)
|
||||||
rexml (3.3.7)
|
rexml (3.3.9)
|
||||||
rotp (6.3.0)
|
rotp (6.3.0)
|
||||||
rouge (4.3.0)
|
rouge (4.3.0)
|
||||||
rpam2 (4.0.2)
|
rpam2 (4.0.2)
|
||||||
@@ -748,15 +752,15 @@ GEM
|
|||||||
parser (>= 3.3.1.0)
|
parser (>= 3.3.1.0)
|
||||||
rubocop-capybara (2.21.0)
|
rubocop-capybara (2.21.0)
|
||||||
rubocop (~> 1.41)
|
rubocop (~> 1.41)
|
||||||
rubocop-performance (1.21.1)
|
rubocop-performance (1.22.1)
|
||||||
rubocop (>= 1.48.1, < 2.0)
|
rubocop (>= 1.48.1, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rails (2.25.1)
|
rubocop-rails (2.26.2)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.33.0, < 2.0)
|
rubocop (>= 1.52.0, < 2.0)
|
||||||
rubocop-ast (>= 1.31.1, < 2.0)
|
rubocop-ast (>= 1.31.1, < 2.0)
|
||||||
rubocop-rspec (3.0.4)
|
rubocop-rspec (3.0.5)
|
||||||
rubocop (~> 1.61)
|
rubocop (~> 1.61)
|
||||||
rubocop-rspec_rails (2.30.0)
|
rubocop-rspec_rails (2.30.0)
|
||||||
rubocop (~> 1.61)
|
rubocop (~> 1.61)
|
||||||
@@ -766,7 +770,7 @@ GEM
|
|||||||
ruby-saml (1.17.0)
|
ruby-saml (1.17.0)
|
||||||
nokogiri (>= 1.13.10)
|
nokogiri (>= 1.13.10)
|
||||||
rexml
|
rexml
|
||||||
ruby-vips (2.2.2)
|
ruby-vips (2.2.3)
|
||||||
ffi (~> 1.12)
|
ffi (~> 1.12)
|
||||||
logger
|
logger
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
@@ -781,6 +785,7 @@ GEM
|
|||||||
scenic (1.8.0)
|
scenic (1.8.0)
|
||||||
activerecord (>= 4.0.0)
|
activerecord (>= 4.0.0)
|
||||||
railties (>= 4.0.0)
|
railties (>= 4.0.0)
|
||||||
|
securerandom (0.4.1)
|
||||||
selenium-webdriver (4.25.0)
|
selenium-webdriver (4.25.0)
|
||||||
base64 (~> 0.2)
|
base64 (~> 0.2)
|
||||||
logger (~> 1.4)
|
logger (~> 1.4)
|
||||||
@@ -837,7 +842,7 @@ GEM
|
|||||||
test-prof (1.4.2)
|
test-prof (1.4.2)
|
||||||
thor (1.3.2)
|
thor (1.3.2)
|
||||||
tilt (2.4.0)
|
tilt (2.4.0)
|
||||||
timeout (0.4.1)
|
timeout (0.4.3)
|
||||||
tpm-key_attestation (0.12.1)
|
tpm-key_attestation (0.12.1)
|
||||||
bindata (~> 2.4)
|
bindata (~> 2.4)
|
||||||
openssl (> 2.0)
|
openssl (> 2.0)
|
||||||
@@ -863,7 +868,7 @@ GEM
|
|||||||
unf_ext
|
unf_ext
|
||||||
unf_ext (0.0.9.1)
|
unf_ext (0.0.9.1)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (2.5.0)
|
||||||
uri (0.13.1)
|
uri (0.13.2)
|
||||||
validate_email (0.1.6)
|
validate_email (0.1.6)
|
||||||
activemodel (>= 3.0)
|
activemodel (>= 3.0)
|
||||||
mail (>= 2.2.5)
|
mail (>= 2.2.5)
|
||||||
@@ -884,7 +889,7 @@ GEM
|
|||||||
webfinger (1.2.0)
|
webfinger (1.2.0)
|
||||||
activesupport
|
activesupport
|
||||||
httpclient (>= 2.4)
|
httpclient (>= 2.4)
|
||||||
webmock (3.23.1)
|
webmock (3.24.0)
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
@@ -1060,4 +1065,4 @@ RUBY VERSION
|
|||||||
ruby 3.3.4p94
|
ruby 3.3.4p94
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.5.18
|
2.6.5
|
||||||
|
|||||||
11
SECURITY.md
11
SECURITY.md
@@ -13,8 +13,9 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
|||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | --------- |
|
| ------- | ---------------- |
|
||||||
| 4.2.x | Yes |
|
| 4.3.x | Yes |
|
||||||
| 4.1.x | Yes |
|
| 4.2.x | Yes |
|
||||||
| < 4.1 | No |
|
| 4.1.x | Until 2025-04-08 |
|
||||||
|
| < 4.1 | No |
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class Admin::AnnouncementsController < Admin::BaseController
|
|||||||
|
|
||||||
def index
|
def index
|
||||||
authorize :announcement, :index?
|
authorize :announcement, :index?
|
||||||
|
@published_announcements_count = Announcement.published.async_count
|
||||||
end
|
end
|
||||||
|
|
||||||
def new
|
def new
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class Admin::Disputes::AppealsController < Admin::BaseController
|
|||||||
def index
|
def index
|
||||||
authorize :appeal, :index?
|
authorize :appeal, :index?
|
||||||
|
|
||||||
|
@pending_appeals_count = Appeal.pending.async_count
|
||||||
@appeals = filtered_appeals.page(params[:page])
|
@appeals = filtered_appeals.page(params[:page])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class Admin::Trends::Links::PreviewCardProvidersController < Admin::BaseControll
|
|||||||
def index
|
def index
|
||||||
authorize :preview_card_provider, :review?
|
authorize :preview_card_provider, :review?
|
||||||
|
|
||||||
|
@pending_preview_card_providers_count = PreviewCardProvider.unreviewed.async_count
|
||||||
@preview_card_providers = filtered_preview_card_providers.page(params[:page])
|
@preview_card_providers = filtered_preview_card_providers.page(params[:page])
|
||||||
@form = Trends::PreviewCardProviderBatch.new
|
@form = Trends::PreviewCardProviderBatch.new
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class Admin::Trends::TagsController < Admin::BaseController
|
|||||||
def index
|
def index
|
||||||
authorize :tag, :review?
|
authorize :tag, :review?
|
||||||
|
|
||||||
|
@pending_tags_count = Tag.pending_review.async_count
|
||||||
@tags = filtered_tags.page(params[:page])
|
@tags = filtered_tags.page(params[:page])
|
||||||
@form = Trends::TagBatch.new
|
@form = Trends::TagBatch.new
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
before_action :check_account_confirmation, except: [:index, :create]
|
before_action :check_account_confirmation, except: [:index, :create]
|
||||||
before_action :check_enabled_registrations, only: [:create]
|
before_action :check_enabled_registrations, only: [:create]
|
||||||
before_action :check_accounts_limit, only: [:index]
|
before_action :check_accounts_limit, only: [:index]
|
||||||
|
before_action :check_following_self, only: [:follow]
|
||||||
|
|
||||||
skip_before_action :require_authenticated_user!, only: :create
|
skip_before_action :require_authenticated_user!, only: :create
|
||||||
|
|
||||||
@@ -101,6 +102,10 @@ class Api::V1::AccountsController < Api::BaseController
|
|||||||
raise(Mastodon::ValidationError) if account_ids.size > DEFAULT_ACCOUNTS_LIMIT
|
raise(Mastodon::ValidationError) if account_ids.size > DEFAULT_ACCOUNTS_LIMIT
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_following_self
|
||||||
|
render json: { error: I18n.t('accounts.self_follow_error') }, status: 403 if current_user.account.id == @account.id
|
||||||
|
end
|
||||||
|
|
||||||
def relationships(**options)
|
def relationships(**options)
|
||||||
AccountRelationshipsPresenter.new([@account], current_user.account_id, **options)
|
AccountRelationshipsPresenter.new([@account], current_user.account_id, **options)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show_domain_blocks_to_user?
|
def show_domain_blocks_to_user?
|
||||||
Setting.show_domain_blocks == 'users' && user_signed_in?
|
Setting.show_domain_blocks == 'users' && user_signed_in? && current_user.functional_or_moved?
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_domain_blocks
|
def set_domain_blocks
|
||||||
@@ -47,6 +47,6 @@ class Api::V1::Instances::DomainBlocksController < Api::V1::Instances::BaseContr
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show_rationale_for_user?
|
def show_rationale_for_user?
|
||||||
Setting.show_domain_blocks_rationale == 'users' && user_signed_in?
|
Setting.show_domain_blocks_rationale == 'users' && user_signed_in? && current_user.functional_or_moved?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -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)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class Api::V2::NotificationsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@notification = current_account.notifications.without_suspended.find_by!(group_key: params[:group_key])
|
@notification = current_account.notifications.without_suspended.by_group_key(params[:group_key]).take!
|
||||||
presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification]))
|
presenter = GroupedNotificationsPresenter.new(NotificationGroup.from_notifications([@notification]))
|
||||||
render json: presenter, serializer: REST::DedupNotificationGroupSerializer
|
render json: presenter, serializer: REST::DedupNotificationGroupSerializer
|
||||||
end
|
end
|
||||||
@@ -57,7 +57,7 @@ class Api::V2::NotificationsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def dismiss
|
def dismiss
|
||||||
current_account.notifications.where(group_key: params[:group_key]).destroy_all
|
current_account.notifications.by_group_key(params[:group_key]).destroy_all
|
||||||
render_empty
|
render_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -80,10 +80,31 @@ class Api::V2::NotificationsController < Api::BaseController
|
|||||||
return [] if @notifications.empty?
|
return [] if @notifications.empty?
|
||||||
|
|
||||||
MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_grouped_notifications') do
|
MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_grouped_notifications') do
|
||||||
NotificationGroup.from_notifications(@notifications, pagination_range: (@notifications.last.id)..(@notifications.first.id), grouped_types: params[:grouped_types])
|
pagination_range = (@notifications.last.id)..@notifications.first.id
|
||||||
|
|
||||||
|
# If the page is incomplete, we know we are on the last page
|
||||||
|
if incomplete_page?
|
||||||
|
if paginating_up?
|
||||||
|
pagination_range = @notifications.last.id...(params[:max_id]&.to_i)
|
||||||
|
else
|
||||||
|
range_start = params[:since_id]&.to_i
|
||||||
|
range_start += 1 unless range_start.nil?
|
||||||
|
pagination_range = range_start..(@notifications.first.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
NotificationGroup.from_notifications(@notifications, pagination_range: pagination_range, grouped_types: params[:grouped_types])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def incomplete_page?
|
||||||
|
@notifications.size < limit_param(DEFAULT_NOTIFICATIONS_LIMIT)
|
||||||
|
end
|
||||||
|
|
||||||
|
def paginating_up?
|
||||||
|
params[:min_id].present?
|
||||||
|
end
|
||||||
|
|
||||||
def browserable_account_notifications
|
def browserable_account_notifications
|
||||||
current_account.notifications.without_suspended.browserable(
|
current_account.notifications.without_suspended.browserable(
|
||||||
types: Array(browserable_params[:types]),
|
types: Array(browserable_params[:types]),
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ module SignatureVerification
|
|||||||
|
|
||||||
def verify_signature_strength!
|
def verify_signature_strength!
|
||||||
raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
|
raise SignatureVerificationError, 'Mastodon requires the Date header or (created) pseudo-header to be signed' unless signed_headers.include?('date') || signed_headers.include?('(created)')
|
||||||
raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(Request::REQUEST_TARGET) || signed_headers.include?('digest')
|
raise SignatureVerificationError, 'Mastodon requires the Digest header or (request-target) pseudo-header to be signed' unless signed_headers.include?(HttpSignatureDraft::REQUEST_TARGET) || signed_headers.include?('digest')
|
||||||
raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host')
|
raise SignatureVerificationError, 'Mastodon requires the Host header to be signed when doing a GET request' if request.get? && !signed_headers.include?('host')
|
||||||
raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
|
raise SignatureVerificationError, 'Mastodon requires the Digest header to be signed when doing a POST request' if request.post? && !signed_headers.include?('digest')
|
||||||
end
|
end
|
||||||
@@ -155,14 +155,14 @@ module SignatureVerification
|
|||||||
def build_signed_string(include_query_string: true)
|
def build_signed_string(include_query_string: true)
|
||||||
signed_headers.map do |signed_header|
|
signed_headers.map do |signed_header|
|
||||||
case signed_header
|
case signed_header
|
||||||
when Request::REQUEST_TARGET
|
when HttpSignatureDraft::REQUEST_TARGET
|
||||||
if include_query_string
|
if include_query_string
|
||||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}"
|
"#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.original_fullpath}"
|
||||||
else
|
else
|
||||||
# Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header.
|
# Current versions of Mastodon incorrectly omit the query string from the (request-target) pseudo-header.
|
||||||
# Therefore, temporarily support such incorrect signatures for compatibility.
|
# Therefore, temporarily support such incorrect signatures for compatibility.
|
||||||
# TODO: remove eventually some time after release of the fixed version
|
# TODO: remove eventually some time after release of the fixed version
|
||||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
"#{HttpSignatureDraft::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||||
end
|
end
|
||||||
when '(created)'
|
when '(created)'
|
||||||
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
raise SignatureVerificationError, 'Invalid pseudo-header (created) for rsa-sha256' unless signature_algorithm == 'hs2019'
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ module WebAppControllerConcern
|
|||||||
policy = ContentSecurityPolicy.new
|
policy = ContentSecurityPolicy.new
|
||||||
|
|
||||||
if policy.sso_host.present?
|
if policy.sso_host.present?
|
||||||
p.form_action policy.sso_host
|
p.form_action policy.sso_host, -> { "https://#{request.host}/auth/auth/" }
|
||||||
else
|
else
|
||||||
p.form_action :none
|
p.form_action :none
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ module Settings
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create
|
def create
|
||||||
session[:new_otp_secret] = User.generate_otp_secret(32)
|
session[:new_otp_secret] = User.generate_otp_secret
|
||||||
|
|
||||||
redirect_to new_settings_two_factor_authentication_confirmation_path
|
redirect_to new_settings_two_factor_authentication_confirmation_path
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,7 +7,23 @@ module WellKnown
|
|||||||
def show
|
def show
|
||||||
@webfinger_template = "#{webfinger_url}?resource={uri}"
|
@webfinger_template = "#{webfinger_url}?resource={uri}"
|
||||||
expires_in 3.days, public: true
|
expires_in 3.days, public: true
|
||||||
render content_type: 'application/xrd+xml', formats: [:xml]
|
|
||||||
|
respond_to do |format|
|
||||||
|
format.any do
|
||||||
|
render content_type: 'application/xrd+xml', formats: [:xml]
|
||||||
|
end
|
||||||
|
|
||||||
|
format.json do
|
||||||
|
render json: {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
rel: 'lrdd',
|
||||||
|
template: @webfinger_template,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -35,4 +35,11 @@ module Admin::ActionLogsHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sorted_action_log_types
|
||||||
|
Admin::ActionLogFilter::ACTION_TYPE_MAP
|
||||||
|
.keys
|
||||||
|
.map { |key| [I18n.t("admin.action_logs.action_types.#{key}"), key] }
|
||||||
|
.sort_by(&:first)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ module Admin::DashboardHelper
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def date_range(range)
|
||||||
|
[l(range.first), l(range.last)]
|
||||||
|
.join(' - ')
|
||||||
|
end
|
||||||
|
|
||||||
def relevant_account_timestamp(account)
|
def relevant_account_timestamp(account)
|
||||||
timestamp, exact = if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago
|
timestamp, exact = if account.user_current_sign_in_at && account.user_current_sign_in_at < 24.hours.ago
|
||||||
[account.user_current_sign_in_at, true]
|
[account.user_current_sign_in_at, true]
|
||||||
@@ -25,6 +30,8 @@ module Admin::DashboardHelper
|
|||||||
[account.user_current_sign_in_at, false]
|
[account.user_current_sign_in_at, false]
|
||||||
elsif account.user_pending?
|
elsif account.user_pending?
|
||||||
[account.user_created_at, true]
|
[account.user_created_at, true]
|
||||||
|
elsif account.suspended_at.present? && account.local? && account.user.nil?
|
||||||
|
[account.suspended_at, true]
|
||||||
elsif account.last_status_at.present?
|
elsif account.last_status_at.present?
|
||||||
[account.last_status_at, true]
|
[account.last_status_at, true]
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ApplicationHelper
|
module ApplicationHelper
|
||||||
DANGEROUS_SCOPES = %w(
|
|
||||||
read
|
|
||||||
write
|
|
||||||
follow
|
|
||||||
).freeze
|
|
||||||
|
|
||||||
RTL_LOCALES = %i(
|
RTL_LOCALES = %i(
|
||||||
ar
|
ar
|
||||||
ckb
|
ckb
|
||||||
@@ -85,7 +79,7 @@ module ApplicationHelper
|
|||||||
|
|
||||||
def html_title
|
def html_title
|
||||||
safe_join(
|
safe_join(
|
||||||
[content_for(:page_title).to_s.chomp, title]
|
[content_for(:page_title), title]
|
||||||
.compact_blank,
|
.compact_blank,
|
||||||
' - '
|
' - '
|
||||||
)
|
)
|
||||||
@@ -95,8 +89,11 @@ module ApplicationHelper
|
|||||||
Rails.env.production? ? site_title : "#{site_title} (Dev)"
|
Rails.env.production? ? site_title : "#{site_title} (Dev)"
|
||||||
end
|
end
|
||||||
|
|
||||||
def class_for_scope(scope)
|
def label_for_scope(scope)
|
||||||
'scope-danger' if DANGEROUS_SCOPES.include?(scope.to_s)
|
safe_join [
|
||||||
|
tag.samp(scope, class: { 'scope-danger' => SessionActivation::DEFAULT_SCOPES.include?(scope.to_s) }),
|
||||||
|
tag.span(t("doorkeeper.scopes.#{scope}"), class: :hint),
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def can?(action, record)
|
def can?(action, record)
|
||||||
@@ -244,6 +241,15 @@ module ApplicationHelper
|
|||||||
full_asset_url(instance_presenter.mascot&.file&.url || frontend_asset_path('images/elephant_ui_plane.svg'))
|
full_asset_url(instance_presenter.mascot&.file&.url || frontend_asset_path('images/elephant_ui_plane.svg'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def copyable_input(options = {})
|
||||||
|
tag.input(type: :text, maxlength: 999, spellcheck: false, readonly: true, **options)
|
||||||
|
end
|
||||||
|
|
||||||
|
def recent_tag_usage(tag)
|
||||||
|
people = tag.history.aggregate(2.days.ago.to_date..Time.zone.today).accounts
|
||||||
|
I18n.t 'user_mailer.welcome.hashtags_recent_count', people: number_with_delimiter(people), count: people
|
||||||
|
end
|
||||||
|
|
||||||
# glitch-soc addition to handle the multiple flavors
|
# 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']
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module WebfingerHelper
|
|
||||||
def webfinger!(uri)
|
|
||||||
Webfinger.new(uri).perform
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -37,8 +37,7 @@ export const synchronouslySubmitMarkers = createAppAsyncThunk(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
} else if ('sendBeacon' in navigator) {
|
||||||
} else if ('navigator' && 'sendBeacon' in navigator) {
|
|
||||||
// Failing that, we can use sendBeacon, but we have to encode the data as
|
// Failing that, we can use sendBeacon, but we have to encode the data as
|
||||||
// FormData for DoorKeeper to recognize the token.
|
// FormData for DoorKeeper to recognize the token.
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|||||||
@@ -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,13 +70,19 @@ function dispatchAssociatedRecords(
|
|||||||
dispatch(importFetchedStatuses(fetchedStatuses));
|
dispatch(importFetchedStatuses(fetchedStatuses));
|
||||||
}
|
}
|
||||||
|
|
||||||
const supportedGroupedNotificationTypes = ['favourite', 'reblog'];
|
function selectNotificationGroupedTypes(state: RootState) {
|
||||||
|
const types: NotificationType[] = ['favourite', 'reblog'];
|
||||||
|
|
||||||
|
if (selectSettingsNotificationsGroupFollows(state)) types.push('follow');
|
||||||
|
|
||||||
|
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 }) => {
|
||||||
@@ -98,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()),
|
||||||
}),
|
}),
|
||||||
@@ -115,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
|
||||||
@@ -133,6 +141,9 @@ export const pollRecentNotifications = createDataLoadingThunk(
|
|||||||
|
|
||||||
return { notifications };
|
return { notifications };
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
useLoadingBar: false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const processNewNotificationForGroups = createAppAsyncThunk(
|
export const processNewNotificationForGroups = createAppAsyncThunk(
|
||||||
@@ -164,7 +175,10 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
|
|||||||
|
|
||||||
dispatchAssociatedRecords(dispatch, [notification]);
|
dispatchAssociatedRecords(dispatch, [notification]);
|
||||||
|
|
||||||
return notification;
|
return {
|
||||||
|
notification,
|
||||||
|
groupedTypes: selectNotificationGroupedTypes(state),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { useHovering } from 'flavours/glitch/hooks/useHovering';
|
||||||
|
import { autoPlayGif } from 'flavours/glitch/initial_state';
|
||||||
import type { Account } from 'flavours/glitch/models/account';
|
import type { Account } from 'flavours/glitch/models/account';
|
||||||
|
|
||||||
import { useHovering } from '../hooks/useHovering';
|
|
||||||
import { autoPlayGif } from '../initial_state';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
||||||
size: number;
|
size: number;
|
||||||
@@ -25,6 +26,8 @@ export const Avatar: React.FC<Props> = ({
|
|||||||
counterBorderColor,
|
counterBorderColor,
|
||||||
}) => {
|
}) => {
|
||||||
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
|
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
...styleFromParent,
|
...styleFromParent,
|
||||||
@@ -37,17 +40,29 @@ export const Avatar: React.FC<Props> = ({
|
|||||||
? account?.get('avatar')
|
? account?.get('avatar')
|
||||||
: account?.get('avatar_static');
|
: account?.get('avatar_static');
|
||||||
|
|
||||||
|
const handleLoad = useCallback(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, [setLoading]);
|
||||||
|
|
||||||
|
const handleError = useCallback(() => {
|
||||||
|
setError(true);
|
||||||
|
}, [setError]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames('account__avatar', {
|
className={classNames('account__avatar', {
|
||||||
'account__avatar-inline': inline,
|
'account__avatar--inline': inline,
|
||||||
|
'account__avatar--loading': loading,
|
||||||
})}
|
})}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
style={style}
|
style={style}
|
||||||
data-avatar-of={account && `@${account.get('acct')}`}
|
data-avatar-of={account && `@${account.get('acct')}`}
|
||||||
>
|
>
|
||||||
{src && <img src={src} alt='' />}
|
{src && !error && (
|
||||||
|
<img src={src} alt='' onLoad={handleLoad} onError={handleError} />
|
||||||
|
)}
|
||||||
|
|
||||||
{counter && (
|
{counter && (
|
||||||
<div
|
<div
|
||||||
className='account__avatar__counter'
|
className='account__avatar__counter'
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export const CollapseButton = ({ collapsed, setCollapsed }) => {
|
|||||||
if (e.button === 0) {
|
if (e.button === 0) {
|
||||||
setCollapsed(!collapsed);
|
setCollapsed(!collapsed);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
}, [collapsed, setCollapsed]);
|
}, [collapsed, setCollapsed]);
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,15 @@ export const ContentWarning: React.FC<{
|
|||||||
aria-expanded={expanded}
|
aria-expanded={expanded}
|
||||||
>
|
>
|
||||||
{expanded ? (
|
{expanded ? (
|
||||||
<FormattedMessage id='status.show_less' defaultMessage='Show less' />
|
<FormattedMessage
|
||||||
|
id='content_warning.hide'
|
||||||
|
defaultMessage='Hide post'
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage id='status.show_more' defaultMessage='Show more' />
|
<FormattedMessage
|
||||||
|
id='content_warning.show_more'
|
||||||
|
defaultMessage='Show more'
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{icons}
|
{icons}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -98,12 +98,12 @@ class Item extends PureComponent {
|
|||||||
height = 50;
|
height = 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachment.get('description')?.length > 0) {
|
|
||||||
badges.push(<AltTextBadge key='alt' description={attachment.get('description')} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||||
|
|
||||||
|
if (description?.length > 0) {
|
||||||
|
badges.push(<AltTextBadge key='alt' description={description} />);
|
||||||
|
}
|
||||||
|
|
||||||
if (attachment.get('type') === 'unknown') {
|
if (attachment.get('type') === 'unknown') {
|
||||||
return (
|
return (
|
||||||
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
||||||
@@ -198,7 +198,7 @@ class Item extends PureComponent {
|
|||||||
|
|
||||||
{visible && thumbnail}
|
{visible && thumbnail}
|
||||||
|
|
||||||
{badges && (
|
{visible && badges && (
|
||||||
<div className='media-gallery__item__badges'>
|
<div className='media-gallery__item__badges'>
|
||||||
{badges}
|
{badges}
|
||||||
</div>
|
</div>
|
||||||
@@ -356,14 +356,14 @@ class MediaGallery extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={computedClass} style={style} ref={this.handleRef}>
|
<div className={computedClass} style={style} ref={this.handleRef}>
|
||||||
|
{children}
|
||||||
|
|
||||||
{(!visible || uncached) && (
|
{(!visible || uncached) && (
|
||||||
<div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}>
|
<div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{(visible && !uncached) && (
|
{(visible && !uncached) && (
|
||||||
<div className='media-gallery__actions'>
|
<div className='media-gallery__actions'>
|
||||||
<button className='media-gallery__actions__pill' onClick={this.handleOpen}><FormattedMessage id='media_gallery.hide' defaultMessage='Hide' /></button>
|
<button className='media-gallery__actions__pill' onClick={this.handleOpen}><FormattedMessage id='media_gallery.hide' defaultMessage='Hide' /></button>
|
||||||
|
|||||||
@@ -372,26 +372,29 @@ class Status extends ImmutablePureComponent {
|
|||||||
const { isCollapsed } = this.state;
|
const { isCollapsed } = this.state;
|
||||||
if (!history) return;
|
if (!history) return;
|
||||||
|
|
||||||
if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey)) {
|
if (e.button !== 0 || e.ctrlKey || e.altKey || e.metaKey) {
|
||||||
if (isCollapsed) this.setCollapsed(false);
|
return;
|
||||||
else if (e.shiftKey) {
|
|
||||||
this.setCollapsed(true);
|
|
||||||
document.getSelection().removeAllRanges();
|
|
||||||
} else if (this.props.onClick) {
|
|
||||||
this.props.onClick();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
if (destination === undefined) {
|
|
||||||
destination = `/@${
|
|
||||||
status.getIn(['reblog', 'account', 'acct'], status.getIn(['account', 'acct']))
|
|
||||||
}/${
|
|
||||||
status.getIn(['reblog', 'id'], status.get('id'))
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
history.push(destination);
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCollapsed) this.setCollapsed(false);
|
||||||
|
else if (e.shiftKey) {
|
||||||
|
this.setCollapsed(true);
|
||||||
|
document.getSelection().removeAllRanges();
|
||||||
|
} else if (this.props.onClick) {
|
||||||
|
this.props.onClick();
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (destination === undefined) {
|
||||||
|
destination = `/@${
|
||||||
|
status.getIn(['reblog', 'account', 'acct'], status.getIn(['account', 'acct']))
|
||||||
|
}/${
|
||||||
|
status.getIn(['reblog', 'id'], status.get('id'))
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
history.push(destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
};
|
};
|
||||||
|
|
||||||
handleToggleMediaVisibility = () => {
|
handleToggleMediaVisibility = () => {
|
||||||
@@ -648,7 +651,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 => (
|
||||||
@@ -806,7 +809,8 @@ class Status extends ImmutablePureComponent {
|
|||||||
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
|
{(connectReply || connectUp || connectToRoot) && <div className={classNames('status__line', { 'status__line--full': connectReply, 'status__line--first': !status.get('in_reply_to_id') && !connectToRoot })} />}
|
||||||
|
|
||||||
{(!muted || !isCollapsed) && (
|
{(!muted || !isCollapsed) && (
|
||||||
<header className='status__info'>
|
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */
|
||||||
|
<header onClick={this.parseClick} className='status__info'>
|
||||||
<StatusHeader
|
<StatusHeader
|
||||||
status={status}
|
status={status}
|
||||||
friend={account}
|
friend={account}
|
||||||
|
|||||||
@@ -233,7 +233,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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,15 +18,10 @@ export default class StatusHeader extends PureComponent {
|
|||||||
parseClick: PropTypes.func.isRequired,
|
parseClick: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handles clicks on account name/image
|
|
||||||
handleClick = (acct, e) => {
|
|
||||||
const { parseClick } = this.props;
|
|
||||||
parseClick(e, `/@${acct}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAccountClick = (e) => {
|
handleAccountClick = (e) => {
|
||||||
const { status } = this.props;
|
const { status, parseClick } = this.props;
|
||||||
this.handleClick(status.getIn(['account', 'acct']), e);
|
parseClick(e, `/@${status.getIn(['account', 'acct'])}`);
|
||||||
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Rendering.
|
// Rendering.
|
||||||
|
|||||||
@@ -27,15 +27,19 @@ class ColumnSettings extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='column-settings'>
|
<div className='column-settings'>
|
||||||
<div className='column-settings__row'>
|
<section>
|
||||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
<div className='column-settings__row'>
|
||||||
</div>
|
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<span className='column-settings__section'><FormattedMessage id='home.column_settings.advanced' defaultMessage='Advanced' /></span>
|
<section>
|
||||||
|
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,30 +97,30 @@ const emojifyTextNode = (node, customEmojis) => {
|
|||||||
const { filename, shortCode } = unicodeMapping[unicode_emoji];
|
const { filename, shortCode } = unicodeMapping[unicode_emoji];
|
||||||
const title = shortCode ? `:${shortCode}:` : '';
|
const title = shortCode ? `:${shortCode}:` : '';
|
||||||
|
|
||||||
replacement = document.createElement('picture');
|
|
||||||
|
|
||||||
const isSystemTheme = !!document.body?.classList.contains('theme-system');
|
const isSystemTheme = !!document.body?.classList.contains('theme-system');
|
||||||
|
|
||||||
if(isSystemTheme) {
|
const theme = (isSystemTheme || document.body?.classList.contains('theme-mastodon-light')) ? 'light' : 'dark';
|
||||||
let source = document.createElement('source');
|
|
||||||
source.setAttribute('media', '(prefers-color-scheme: dark)');
|
|
||||||
source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, "dark")}.svg`);
|
|
||||||
replacement.appendChild(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
let img = document.createElement('img');
|
const imageFilename = emojiFilename(filename, theme);
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
img.setAttribute('draggable', 'false');
|
img.setAttribute('draggable', 'false');
|
||||||
img.setAttribute('class', 'emojione');
|
img.setAttribute('class', 'emojione');
|
||||||
img.setAttribute('alt', unicode_emoji);
|
img.setAttribute('alt', unicode_emoji);
|
||||||
img.setAttribute('title', title);
|
img.setAttribute('title', title);
|
||||||
|
img.setAttribute('src', `${assetHost}/emoji/${imageFilename}.svg`);
|
||||||
|
|
||||||
let theme = "light";
|
if (isSystemTheme && imageFilename !== emojiFilename(filename, 'dark')) {
|
||||||
|
replacement = document.createElement('picture');
|
||||||
|
|
||||||
if(!isSystemTheme && !document.body?.classList.contains('skin-mastodon-light'))
|
const source = document.createElement('source');
|
||||||
theme = "dark";
|
source.setAttribute('media', '(prefers-color-scheme: dark)');
|
||||||
|
source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, 'dark')}.svg`);
|
||||||
img.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename, theme)}.svg`);
|
replacement.appendChild(source);
|
||||||
replacement.appendChild(img);
|
replacement.appendChild(img);
|
||||||
|
} else {
|
||||||
|
replacement = img;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the processed-up-to-now string and the emoji replacement
|
// Add the processed-up-to-now string and the emoji replacement
|
||||||
@@ -135,7 +135,7 @@ const emojifyTextNode = (node, customEmojis) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const emojifyNode = (node, customEmojis) => {
|
const emojifyNode = (node, customEmojis) => {
|
||||||
for (const child of node.childNodes) {
|
for (const child of Array.from(node.childNodes)) {
|
||||||
switch(child.nodeType) {
|
switch(child.nodeType) {
|
||||||
case Node.TEXT_NODE:
|
case Node.TEXT_NODE:
|
||||||
emojifyTextNode(child, customEmojis);
|
emojifyTextNode(child, customEmojis);
|
||||||
|
|||||||
@@ -129,8 +129,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
|
||||||
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
|
||||||
|
setCanScrollRight(bodyRef.current.scrollLeft < 0);
|
||||||
|
} else {
|
||||||
|
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
||||||
|
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
||||||
|
}
|
||||||
}, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
|
}, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
|
||||||
|
|
||||||
const handleLeftNav = useCallback(() => {
|
const handleLeftNav = useCallback(() => {
|
||||||
@@ -146,8 +151,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
|
||||||
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
|
||||||
|
setCanScrollRight(bodyRef.current.scrollLeft < 0);
|
||||||
|
} else {
|
||||||
|
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
||||||
|
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
||||||
|
}
|
||||||
}, [setCanScrollRight, setCanScrollLeft, bodyRef]);
|
}, [setCanScrollRight, setCanScrollLeft, bodyRef]);
|
||||||
|
|
||||||
const handleDismiss = useCallback(() => {
|
const handleDismiss = useCallback(() => {
|
||||||
|
|||||||
@@ -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'>
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
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 +26,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 +51,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 +77,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}
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Icon } from 'flavours/glitch/components/icon';
|
|||||||
import {
|
import {
|
||||||
selectSettingsNotificationsQuickFilterActive,
|
selectSettingsNotificationsQuickFilterActive,
|
||||||
selectSettingsNotificationsQuickFilterAdvanced,
|
selectSettingsNotificationsQuickFilterAdvanced,
|
||||||
|
selectSettingsNotificationsQuickFilterShow,
|
||||||
} from 'flavours/glitch/selectors/settings';
|
} from 'flavours/glitch/selectors/settings';
|
||||||
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
||||||
|
|
||||||
@@ -65,6 +66,11 @@ export const FilterBar: React.FC = () => {
|
|||||||
const advancedMode = useAppSelector(
|
const advancedMode = useAppSelector(
|
||||||
selectSettingsNotificationsQuickFilterAdvanced,
|
selectSettingsNotificationsQuickFilterAdvanced,
|
||||||
);
|
);
|
||||||
|
const useFilterBar = useAppSelector(
|
||||||
|
selectSettingsNotificationsQuickFilterShow,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!useFilterBar) return null;
|
||||||
|
|
||||||
if (advancedMode)
|
if (advancedMode)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export const DetailedStatus: React.FC<{
|
|||||||
) {
|
) {
|
||||||
media.push(<AttachmentList media={status.get('media_attachments')} />);
|
media.push(<AttachmentList media={status.get('media_attachments')} />);
|
||||||
} else if (
|
} else if (
|
||||||
['image', 'gifv'].includes(
|
['image', 'gifv', 'unknown'].includes(
|
||||||
status.getIn(['media_attachments', 0, 'type']) as string,
|
status.getIn(['media_attachments', 0, 'type']) as string,
|
||||||
) ||
|
) ||
|
||||||
status.get('media_attachments').size > 1
|
status.get('media_attachments').size > 1
|
||||||
@@ -259,6 +259,7 @@ export const DetailedStatus: React.FC<{
|
|||||||
src={attachment.get('url')}
|
src={attachment.get('url')}
|
||||||
alt={description}
|
alt={description}
|
||||||
lang={language}
|
lang={language}
|
||||||
|
inline
|
||||||
width={300}
|
width={300}
|
||||||
height={150}
|
height={150}
|
||||||
onOpenVideo={handleOpenVideo}
|
onOpenVideo={handleOpenVideo}
|
||||||
@@ -272,12 +273,12 @@ export const DetailedStatus: React.FC<{
|
|||||||
);
|
);
|
||||||
mediaIcons.push('video-camera');
|
mediaIcons.push('video-camera');
|
||||||
}
|
}
|
||||||
} else if (status.get('spoiler_text').length === 0) {
|
} else if (status.get('card')) {
|
||||||
media.push(
|
media.push(
|
||||||
<Card
|
<Card
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
onOpenMedia={onOpenMedia}
|
onOpenMedia={onOpenMedia}
|
||||||
card={status.get('card', null)}
|
card={status.get('card')}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
mediaIcons.push('link');
|
mediaIcons.push('link');
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { Permalink } from 'flavours/glitch/components/permalink';
|
|||||||
import { PictureInPicture } from 'flavours/glitch/features/picture_in_picture';
|
import { PictureInPicture } from 'flavours/glitch/features/picture_in_picture';
|
||||||
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
import { identityContextPropShape, withIdentity } from 'flavours/glitch/identity_context';
|
||||||
import { layoutFromWindow } from 'flavours/glitch/is_mobile';
|
import { layoutFromWindow } from 'flavours/glitch/is_mobile';
|
||||||
|
import { selectUnreadNotificationGroupsCount } from 'flavours/glitch/selectors/notifications';
|
||||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||||
|
|
||||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
||||||
@@ -90,7 +91,7 @@ const mapStateToProps = state => ({
|
|||||||
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||||
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
||||||
isWide: state.getIn(['local_settings', 'stretch']),
|
isWide: state.getIn(['local_settings', 'stretch']),
|
||||||
unreadNotifications: state.getIn(['notifications', 'unread']),
|
unreadNotifications: selectUnreadNotificationGroupsCount(state),
|
||||||
showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
|
showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']),
|
||||||
hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']),
|
hicolorPrivacyIcons: state.getIn(['local_settings', 'hicolor_privacy_icons']),
|
||||||
moved: state.getIn(['accounts', me, 'moved']) && state.getIn(['accounts', state.getIn(['accounts', me, 'moved'])]),
|
moved: state.getIn(['accounts', me, 'moved']) && state.getIn(['accounts', state.getIn(['accounts', me, 'moved'])]),
|
||||||
|
|||||||
@@ -154,7 +154,5 @@
|
|||||||
"status.is_poll": "This toot is a poll",
|
"status.is_poll": "This toot is a poll",
|
||||||
"status.local_only": "Only visible from your instance",
|
"status.local_only": "Only visible from your instance",
|
||||||
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const NOTIFICATIONS_GROUP_MAX_AVATARS = 8;
|
|||||||
interface BaseNotificationGroup
|
interface BaseNotificationGroup
|
||||||
extends Omit<BaseNotificationGroupJSON, 'sample_account_ids'> {
|
extends Omit<BaseNotificationGroupJSON, 'sample_account_ids'> {
|
||||||
sampleAccountIds: string[];
|
sampleAccountIds: string[];
|
||||||
|
partial: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseNotificationWithStatus<Type extends NotificationWithStatusType>
|
interface BaseNotificationWithStatus<Type extends NotificationWithStatusType>
|
||||||
@@ -128,6 +129,7 @@ export function createNotificationGroupFromJSON(
|
|||||||
return {
|
return {
|
||||||
statusId: statusId ?? undefined,
|
statusId: statusId ?? undefined,
|
||||||
sampleAccountIds,
|
sampleAccountIds,
|
||||||
|
partial: false,
|
||||||
...groupWithoutStatus,
|
...groupWithoutStatus,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -136,12 +138,14 @@ export function createNotificationGroupFromJSON(
|
|||||||
return {
|
return {
|
||||||
report: createReportFromJSON(report),
|
report: createReportFromJSON(report),
|
||||||
sampleAccountIds,
|
sampleAccountIds,
|
||||||
|
partial: false,
|
||||||
...groupWithoutTargetAccount,
|
...groupWithoutTargetAccount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case 'severed_relationships':
|
case 'severed_relationships':
|
||||||
return {
|
return {
|
||||||
...group,
|
...group,
|
||||||
|
partial: false,
|
||||||
event: createAccountRelationshipSeveranceEventFromJSON(group.event),
|
event: createAccountRelationshipSeveranceEventFromJSON(group.event),
|
||||||
sampleAccountIds,
|
sampleAccountIds,
|
||||||
};
|
};
|
||||||
@@ -150,13 +154,16 @@ export function createNotificationGroupFromJSON(
|
|||||||
const { moderation_warning, ...groupWithoutModerationWarning } = group;
|
const { moderation_warning, ...groupWithoutModerationWarning } = group;
|
||||||
return {
|
return {
|
||||||
...groupWithoutModerationWarning,
|
...groupWithoutModerationWarning,
|
||||||
|
partial: false,
|
||||||
moderationWarning: createAccountWarningFromJSON(moderation_warning),
|
moderationWarning: createAccountWarningFromJSON(moderation_warning),
|
||||||
sampleAccountIds,
|
sampleAccountIds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
sampleAccountIds,
|
sampleAccountIds,
|
||||||
|
partial: false,
|
||||||
...group,
|
...group,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -164,17 +171,17 @@ export function createNotificationGroupFromJSON(
|
|||||||
|
|
||||||
export function createNotificationGroupFromNotificationJSON(
|
export function createNotificationGroupFromNotificationJSON(
|
||||||
notification: ApiNotificationJSON,
|
notification: ApiNotificationJSON,
|
||||||
) {
|
): NotificationGroup {
|
||||||
const group = {
|
const group = {
|
||||||
sampleAccountIds: [notification.account.id],
|
sampleAccountIds: [notification.account.id],
|
||||||
group_key: notification.group_key,
|
group_key: notification.group_key,
|
||||||
notifications_count: 1,
|
notifications_count: 1,
|
||||||
type: notification.type,
|
|
||||||
most_recent_notification_id: notification.id,
|
most_recent_notification_id: notification.id,
|
||||||
page_min_id: notification.id,
|
page_min_id: notification.id,
|
||||||
page_max_id: notification.id,
|
page_max_id: notification.id,
|
||||||
latest_page_notification_at: notification.created_at,
|
latest_page_notification_at: notification.created_at,
|
||||||
} as NotificationGroup;
|
partial: true,
|
||||||
|
};
|
||||||
|
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
case 'favourite':
|
case 'favourite':
|
||||||
@@ -183,12 +190,21 @@ export function createNotificationGroupFromNotificationJSON(
|
|||||||
case 'mention':
|
case 'mention':
|
||||||
case 'poll':
|
case 'poll':
|
||||||
case 'update':
|
case 'update':
|
||||||
return { ...group, statusId: notification.status?.id };
|
return {
|
||||||
|
...group,
|
||||||
|
type: notification.type,
|
||||||
|
statusId: notification.status?.id,
|
||||||
|
};
|
||||||
case 'admin.report':
|
case 'admin.report':
|
||||||
return { ...group, report: createReportFromJSON(notification.report) };
|
return {
|
||||||
|
...group,
|
||||||
|
type: notification.type,
|
||||||
|
report: createReportFromJSON(notification.report),
|
||||||
|
};
|
||||||
case 'severed_relationships':
|
case 'severed_relationships':
|
||||||
return {
|
return {
|
||||||
...group,
|
...group,
|
||||||
|
type: notification.type,
|
||||||
event: createAccountRelationshipSeveranceEventFromJSON(
|
event: createAccountRelationshipSeveranceEventFromJSON(
|
||||||
notification.event,
|
notification.event,
|
||||||
),
|
),
|
||||||
@@ -196,11 +212,15 @@ export function createNotificationGroupFromNotificationJSON(
|
|||||||
case 'moderation_warning':
|
case 'moderation_warning':
|
||||||
return {
|
return {
|
||||||
...group,
|
...group,
|
||||||
|
type: notification.type,
|
||||||
moderationWarning: createAccountWarningFromJSON(
|
moderationWarning: createAccountWarningFromJSON(
|
||||||
notification.moderation_warning,
|
notification.moderation_warning,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
return group;
|
return {
|
||||||
|
...group,
|
||||||
|
type: notification.type,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -330,12 +330,26 @@ const expiresInFromExpiresAt = expires_at => {
|
|||||||
|
|
||||||
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
|
const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
|
||||||
prefix = prefix.toLowerCase();
|
prefix = prefix.toLowerCase();
|
||||||
|
|
||||||
if (suggestions.length < 4) {
|
if (suggestions.length < 4) {
|
||||||
const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase()));
|
const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase()));
|
||||||
return suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
|
suggestions = suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
|
||||||
} else {
|
|
||||||
return suggestions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prefer capitalization from personal history, unless personal history is all lower-case
|
||||||
|
const fixSuggestionCapitalization = (suggestion) => {
|
||||||
|
if (suggestion.type !== 'hashtag')
|
||||||
|
return suggestion;
|
||||||
|
|
||||||
|
const tagFromHistory = tagHistory.find((tag) => tag.localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) === 0);
|
||||||
|
|
||||||
|
if (!tagFromHistory || tagFromHistory.toLowerCase() === tagFromHistory)
|
||||||
|
return suggestion;
|
||||||
|
|
||||||
|
return { ...suggestion, name: tagFromHistory };
|
||||||
|
};
|
||||||
|
|
||||||
|
return suggestions.map(fixSuggestionCapitalization);
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => {
|
const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => {
|
||||||
@@ -507,7 +521,7 @@ export default function compose(state = initialState, action) {
|
|||||||
.set('isUploadingThumbnail', false)
|
.set('isUploadingThumbnail', false)
|
||||||
.update('media_attachments', list => list.map(item => {
|
.update('media_attachments', list => list.map(item => {
|
||||||
if (item.get('id') === action.media.id) {
|
if (item.get('id') === action.media.id) {
|
||||||
return fromJS(action.media);
|
return fromJS(action.media).set('unattached', item.get('unattached'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
|
|||||||
@@ -29,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';
|
||||||
@@ -204,7 +205,15 @@ function mergeGapsAround(
|
|||||||
function processNewNotification(
|
function processNewNotification(
|
||||||
groups: NotificationGroupsState['groups'],
|
groups: NotificationGroupsState['groups'],
|
||||||
notification: ApiNotificationJSON,
|
notification: ApiNotificationJSON,
|
||||||
|
groupedTypes: NotificationType[],
|
||||||
) {
|
) {
|
||||||
|
if (!groupedTypes.includes(notification.type)) {
|
||||||
|
notification = {
|
||||||
|
...notification,
|
||||||
|
group_key: `ungrouped-${notification.id}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const existingGroupIndex = groups.findIndex(
|
const existingGroupIndex = groups.findIndex(
|
||||||
(group) =>
|
(group) =>
|
||||||
group.type !== 'gap' && group.group_key === notification.group_key,
|
group.type !== 'gap' && group.group_key === notification.group_key,
|
||||||
@@ -242,7 +251,7 @@ function processNewNotification(
|
|||||||
groups.unshift(existingGroup);
|
groups.unshift(existingGroup);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create a new group
|
// We have not found an existing group, create a new one
|
||||||
groups.unshift(createNotificationGroupFromNotificationJSON(notification));
|
groups.unshift(createNotificationGroupFromNotificationJSON(notification));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,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);
|
||||||
@@ -523,10 +534,13 @@ export const notificationGroupsReducer = createReducer<NotificationGroupsState>(
|
|||||||
if (existingGroupIndex > -1) {
|
if (existingGroupIndex > -1) {
|
||||||
const existingGroup = state.groups[existingGroupIndex];
|
const existingGroup = state.groups[existingGroupIndex];
|
||||||
if (existingGroup && existingGroup.type !== 'gap') {
|
if (existingGroup && existingGroup.type !== 'gap') {
|
||||||
group.notifications_count += existingGroup.notifications_count;
|
if (group.partial) {
|
||||||
group.sampleAccountIds = group.sampleAccountIds
|
group.notifications_count +=
|
||||||
.concat(existingGroup.sampleAccountIds)
|
existingGroup.notifications_count;
|
||||||
.slice(0, NOTIFICATIONS_GROUP_MAX_AVATARS);
|
group.sampleAccountIds = group.sampleAccountIds
|
||||||
|
.concat(existingGroup.sampleAccountIds)
|
||||||
|
.slice(0, NOTIFICATIONS_GROUP_MAX_AVATARS);
|
||||||
|
}
|
||||||
state.groups.splice(existingGroupIndex, 1);
|
state.groups.splice(existingGroupIndex, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -551,7 +565,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 (
|
||||||
|
|||||||
@@ -79,6 +79,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({
|
||||||
|
|||||||
50
app/javascript/flavours/glitch/selectors/filters.ts
Normal file
50
app/javascript/flavours/glitch/selectors/filters.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import type { RootState } from 'flavours/glitch/store';
|
||||||
|
import { toServerSideType } from 'flavours/glitch/utils/filters';
|
||||||
|
|
||||||
|
// TODO: move to `app/javascript/flavours/glitch/models` and use more globally
|
||||||
|
type Filter = Immutable.Map<string, unknown>;
|
||||||
|
|
||||||
|
// TODO: move to `app/javascript/flavours/glitch/models` and use more globally
|
||||||
|
type FilterResult = Immutable.Map<string, unknown>;
|
||||||
|
|
||||||
|
export const getFilters = createSelector(
|
||||||
|
[
|
||||||
|
(state: RootState) => state.filters as Immutable.Map<string, Filter>,
|
||||||
|
(_, { contextType }: { contextType: string }) => contextType,
|
||||||
|
],
|
||||||
|
(filters, contextType) => {
|
||||||
|
if (!contextType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const serverSideType = toServerSideType(contextType);
|
||||||
|
|
||||||
|
return filters.filter((filter) => {
|
||||||
|
const context = filter.get('context') as Immutable.List<string>;
|
||||||
|
const expiration = filter.get('expires_at') as Date | null;
|
||||||
|
return (
|
||||||
|
context.includes(serverSideType) &&
|
||||||
|
(expiration === null || expiration > now)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getStatusHidden = (
|
||||||
|
state: RootState,
|
||||||
|
{ id, contextType }: { id: string; contextType: string },
|
||||||
|
) => {
|
||||||
|
const filters = getFilters(state, { contextType });
|
||||||
|
if (filters === null) return false;
|
||||||
|
|
||||||
|
const filtered = state.statuses.getIn([id, 'filtered']) as
|
||||||
|
| Immutable.List<FilterResult>
|
||||||
|
| undefined;
|
||||||
|
return filtered?.some(
|
||||||
|
(result) =>
|
||||||
|
filters.getIn([result.get('filter'), 'filter_action']) === 'hide',
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,23 +1,12 @@
|
|||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { 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(
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -226,6 +226,10 @@ $content-width: 840px;
|
|||||||
gap: 5px;
|
gap: 5px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
@media screen and (max-width: $mobile-breakpoint) {
|
||||||
|
flex: 1 0 50%;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:active {
|
&:active {
|
||||||
@@ -1046,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);
|
||||||
@@ -1075,6 +1085,10 @@ a.name-tag,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
a.announcements-list__item__title {
|
a.announcements-list__item__title {
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus,
|
&:focus,
|
||||||
|
|||||||
@@ -2259,7 +2259,6 @@ body > [data-popper-placement] {
|
|||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: var(--avatar-border-radius);
|
border-radius: var(--avatar-border-radius);
|
||||||
background-color: var(--surface-background-color);
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -2269,7 +2268,11 @@ body > [data-popper-placement] {
|
|||||||
display: inline-block; // to not show broken images
|
display: inline-block; // to not show broken images
|
||||||
}
|
}
|
||||||
|
|
||||||
&-inline {
|
&--loading {
|
||||||
|
background-color: var(--surface-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--inline {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-inline-end: 5px;
|
margin-inline-end: 5px;
|
||||||
@@ -2938,6 +2941,7 @@ a.account__display-name {
|
|||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
contain: inline-size layout paint style;
|
||||||
|
|
||||||
@media screen and (min-width: $no-gap-breakpoint) {
|
@media screen and (min-width: $no-gap-breakpoint) {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
@@ -3793,6 +3797,7 @@ $ui-header-logo-wordmark-width: 99px;
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drawer__inner__mastodon {
|
.drawer__inner__mastodon {
|
||||||
@@ -4233,6 +4238,7 @@ input.glitch-setting-text {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid var(--background-border-color);
|
border: 1px solid var(--background-border-color);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
contain: inline-size layout paint style;
|
||||||
|
|
||||||
&.bottomless {
|
&.bottomless {
|
||||||
border-radius: 8px 8px 0 0;
|
border-radius: 8px 8px 0 0;
|
||||||
@@ -6271,6 +6277,7 @@ a.status-card {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
max-width: 100vw;
|
||||||
|
|
||||||
@media screen and (width <= $mobile-breakpoint) {
|
@media screen and (width <= $mobile-breakpoint) {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
@@ -7633,6 +7640,8 @@ img.modal-warning {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--layout-3 {
|
&--layout-3 {
|
||||||
|
min-height: calc(64px * 2 + 8px);
|
||||||
|
|
||||||
& > .media-gallery__item:nth-child(1) {
|
& > .media-gallery__item:nth-child(1) {
|
||||||
border-end-end-radius: 0;
|
border-end-end-radius: 0;
|
||||||
border-start-end-radius: 0;
|
border-start-end-radius: 0;
|
||||||
@@ -7652,6 +7661,8 @@ img.modal-warning {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--layout-4 {
|
&--layout-4 {
|
||||||
|
min-height: calc(64px * 2 + 8px);
|
||||||
|
|
||||||
& > .media-gallery__item:nth-child(1) {
|
& > .media-gallery__item:nth-child(1) {
|
||||||
border-end-end-radius: 0;
|
border-end-end-radius: 0;
|
||||||
border-start-end-radius: 0;
|
border-start-end-radius: 0;
|
||||||
@@ -8521,79 +8532,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;
|
||||||
@@ -8614,6 +8569,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11171,6 +11172,7 @@ noscript {
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
}
|
}
|
||||||
@@ -11359,21 +11361,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 {
|
||||||
@@ -11657,19 +11655,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 {
|
||||||
@@ -11678,31 +11678,16 @@ noscript {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::before,
|
&--filter {
|
||||||
&::after {
|
color: $darker-text-color;
|
||||||
content: '';
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
background: url('~images/warning-stripes.svg') repeat-y;
|
|
||||||
width: 5px;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
p {
|
||||||
border-start-start-radius: 4px;
|
font-weight: normal;
|
||||||
border-end-start-radius: 4px;
|
}
|
||||||
inset-inline-start: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
.filter-name {
|
||||||
border-start-end-radius: 4px;
|
font-weight: 500;
|
||||||
border-end-end-radius: 4px;
|
color: $secondary-text-color;
|
||||||
inset-inline-end: 0;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&--filter::before,
|
|
||||||
&--filter::after {
|
|
||||||
background-image: url('~images/filter-stripes.svg');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: '';
|
||||||
@@ -666,6 +668,10 @@ code {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-card {
|
||||||
|
contain: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-icon {
|
.block-icon {
|
||||||
|
|||||||
@@ -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>")
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ a.table-action-link {
|
|||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
color: $darker-text-color;
|
color: $darker-text-color;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $highlight-text-color;
|
color: $highlight-text-color;
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.account {
|
.account {
|
||||||
|
max-width: calc(56px + 30ch);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ export const synchronouslySubmitMarkers = createAppAsyncThunk(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
} else if ('sendBeacon' in navigator) {
|
||||||
} else if ('navigator' && 'sendBeacon' in navigator) {
|
|
||||||
// Failing that, we can use sendBeacon, but we have to encode the data as
|
// Failing that, we can use sendBeacon, but we have to encode the data as
|
||||||
// FormData for DoorKeeper to recognize the token.
|
// FormData for DoorKeeper to recognize the token.
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
|||||||
@@ -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,13 +70,19 @@ function dispatchAssociatedRecords(
|
|||||||
dispatch(importFetchedStatuses(fetchedStatuses));
|
dispatch(importFetchedStatuses(fetchedStatuses));
|
||||||
}
|
}
|
||||||
|
|
||||||
const supportedGroupedNotificationTypes = ['favourite', 'reblog'];
|
function selectNotificationGroupedTypes(state: RootState) {
|
||||||
|
const types: NotificationType[] = ['favourite', 'reblog'];
|
||||||
|
|
||||||
|
if (selectSettingsNotificationsGroupFollows(state)) types.push('follow');
|
||||||
|
|
||||||
|
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 }) => {
|
||||||
@@ -98,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()),
|
||||||
}),
|
}),
|
||||||
@@ -115,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
|
||||||
@@ -133,6 +141,9 @@ export const pollRecentNotifications = createDataLoadingThunk(
|
|||||||
|
|
||||||
return { notifications };
|
return { notifications };
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
useLoadingBar: false,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const processNewNotificationForGroups = createAppAsyncThunk(
|
export const processNewNotificationForGroups = createAppAsyncThunk(
|
||||||
@@ -144,7 +155,7 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
|
|||||||
|
|
||||||
const showInColumn =
|
const showInColumn =
|
||||||
activeFilter === 'all'
|
activeFilter === 'all'
|
||||||
? notificationShows[notification.type]
|
? notificationShows[notification.type] !== false
|
||||||
: activeFilter === notification.type;
|
: activeFilter === notification.type;
|
||||||
|
|
||||||
if (!showInColumn) return;
|
if (!showInColumn) return;
|
||||||
@@ -164,7 +175,10 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
|
|||||||
|
|
||||||
dispatchAssociatedRecords(dispatch, [notification]);
|
dispatchAssociatedRecords(dispatch, [notification]);
|
||||||
|
|
||||||
return notification;
|
return {
|
||||||
|
notification,
|
||||||
|
groupedTypes: selectNotificationGroupedTypes(state),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
||||||
<div
|
<div
|
||||||
className="account__avatar"
|
className="account__avatar account__avatar--loading"
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
style={
|
style={
|
||||||
@@ -14,6 +14,8 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
|
onError={[Function]}
|
||||||
|
onLoad={[Function]}
|
||||||
src="/animated/alice.gif"
|
src="/animated/alice.gif"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,7 +23,7 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
|
|||||||
|
|
||||||
exports[`<Avatar /> Still renders a still avatar 1`] = `
|
exports[`<Avatar /> Still renders a still avatar 1`] = `
|
||||||
<div
|
<div
|
||||||
className="account__avatar"
|
className="account__avatar account__avatar--loading"
|
||||||
onMouseEnter={[Function]}
|
onMouseEnter={[Function]}
|
||||||
onMouseLeave={[Function]}
|
onMouseLeave={[Function]}
|
||||||
style={
|
style={
|
||||||
@@ -33,6 +35,8 @@ exports[`<Avatar /> Still renders a still avatar 1`] = `
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
|
onError={[Function]}
|
||||||
|
onLoad={[Function]}
|
||||||
src="/static/alice.jpg"
|
src="/static/alice.jpg"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
import { useState, useCallback } from 'react';
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { useHovering } from 'mastodon/../hooks/useHovering';
|
||||||
|
import { autoPlayGif } from 'mastodon/initial_state';
|
||||||
import type { Account } from 'mastodon/models/account';
|
import type { Account } from 'mastodon/models/account';
|
||||||
|
|
||||||
import { useHovering } from '../../hooks/useHovering';
|
|
||||||
import { autoPlayGif } from '../initial_state';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
account: Account | undefined; // FIXME: remove `undefined` once we know for sure its always there
|
||||||
size: number;
|
size: number;
|
||||||
@@ -25,6 +26,8 @@ export const Avatar: React.FC<Props> = ({
|
|||||||
counterBorderColor,
|
counterBorderColor,
|
||||||
}) => {
|
}) => {
|
||||||
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
|
const { hovering, handleMouseEnter, handleMouseLeave } = useHovering(animate);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
...styleFromParent,
|
...styleFromParent,
|
||||||
@@ -37,16 +40,28 @@ export const Avatar: React.FC<Props> = ({
|
|||||||
? account?.get('avatar')
|
? account?.get('avatar')
|
||||||
: account?.get('avatar_static');
|
: account?.get('avatar_static');
|
||||||
|
|
||||||
|
const handleLoad = useCallback(() => {
|
||||||
|
setLoading(false);
|
||||||
|
}, [setLoading]);
|
||||||
|
|
||||||
|
const handleError = useCallback(() => {
|
||||||
|
setError(true);
|
||||||
|
}, [setError]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames('account__avatar', {
|
className={classNames('account__avatar', {
|
||||||
'account__avatar-inline': inline,
|
'account__avatar--inline': inline,
|
||||||
|
'account__avatar--loading': loading,
|
||||||
})}
|
})}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
{src && <img src={src} alt='' />}
|
{src && !error && (
|
||||||
|
<img src={src} alt='' onLoad={handleLoad} onError={handleError} />
|
||||||
|
)}
|
||||||
|
|
||||||
{counter && (
|
{counter && (
|
||||||
<div
|
<div
|
||||||
className='account__avatar__counter'
|
className='account__avatar__counter'
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -97,12 +97,12 @@ class Item extends PureComponent {
|
|||||||
height = 50;
|
height = 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachment.get('description')?.length > 0) {
|
|
||||||
badges.push(<AltTextBadge key='alt' description={attachment.get('description')} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
const description = attachment.getIn(['translation', 'description']) || attachment.get('description');
|
||||||
|
|
||||||
|
if (description?.length > 0) {
|
||||||
|
badges.push(<AltTextBadge key='alt' description={description} />);
|
||||||
|
}
|
||||||
|
|
||||||
if (attachment.get('type') === 'unknown') {
|
if (attachment.get('type') === 'unknown') {
|
||||||
return (
|
return (
|
||||||
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
<div className={classNames('media-gallery__item', { standalone, 'media-gallery__item--tall': height === 100, 'media-gallery__item--wide': width === 100 })} key={attachment.get('id')}>
|
||||||
@@ -196,7 +196,7 @@ class Item extends PureComponent {
|
|||||||
|
|
||||||
{visible && thumbnail}
|
{visible && thumbnail}
|
||||||
|
|
||||||
{badges && (
|
{visible && badges && (
|
||||||
<div className='media-gallery__item__badges'>
|
<div className='media-gallery__item__badges'>
|
||||||
{badges}
|
{badges}
|
||||||
</div>
|
</div>
|
||||||
@@ -336,14 +336,14 @@ class MediaGallery extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`media-gallery media-gallery--layout-${size}`} style={style} ref={this.handleRef}>
|
<div className={`media-gallery media-gallery--layout-${size}`} style={style} ref={this.handleRef}>
|
||||||
|
{children}
|
||||||
|
|
||||||
{(!visible || uncached) && (
|
{(!visible || uncached) && (
|
||||||
<div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}>
|
<div className={classNames('spoiler-button', { 'spoiler-button--click-thru': uncached })}>
|
||||||
{spoilerButton}
|
{spoilerButton}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{(visible && !uncached) && (
|
{(visible && !uncached) && (
|
||||||
<div className='media-gallery__actions'>
|
<div className='media-gallery__actions'>
|
||||||
<button className='media-gallery__actions__pill' onClick={this.handleOpen}><FormattedMessage id='media_gallery.hide' defaultMessage='Hide' /></button>
|
<button className='media-gallery__actions__pill' onClick={this.handleOpen}><FormattedMessage id='media_gallery.hide' defaultMessage='Hide' /></button>
|
||||||
|
|||||||
@@ -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 => (
|
||||||
@@ -520,7 +520,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
</Bundle>
|
</Bundle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
|
} else if (status.get('card')) {
|
||||||
media = (
|
media = (
|
||||||
<Card
|
<Card
|
||||||
onOpenMedia={this.handleOpenMedia}
|
onOpenMedia={this.handleOpenMedia}
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ class ColumnSettings extends PureComponent {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='column-settings'>
|
<div className='column-settings'>
|
||||||
<div className='column-settings__row'>
|
<section>
|
||||||
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
<div className='column-settings__row'>
|
||||||
</div>
|
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media only' />} />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,23 +22,23 @@ describe('emoji', () => {
|
|||||||
|
|
||||||
it('does unicode', () => {
|
it('does unicode', () => {
|
||||||
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
|
expect(emojify('\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66')).toEqual(
|
||||||
'<picture><img draggable="false" class="emojione" alt="👩👩👦👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg"></picture>');
|
'<img draggable="false" class="emojione" alt="👩👩👦👦" title=":woman-woman-boy-boy:" src="/emoji/1f469-200d-1f469-200d-1f466-200d-1f466.svg">');
|
||||||
expect(emojify('👨👩👧👧')).toEqual(
|
expect(emojify('👨👩👧👧')).toEqual(
|
||||||
'<picture><img draggable="false" class="emojione" alt="👨👩👧👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg"></picture>');
|
'<img draggable="false" class="emojione" alt="👨👩👧👧" title=":man-woman-girl-girl:" src="/emoji/1f468-200d-1f469-200d-1f467-200d-1f467.svg">');
|
||||||
expect(emojify('👩👩👦')).toEqual('<picture><img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg"></picture>');
|
expect(emojify('👩👩👦')).toEqual('<img draggable="false" class="emojione" alt="👩👩👦" title=":woman-woman-boy:" src="/emoji/1f469-200d-1f469-200d-1f466.svg">');
|
||||||
expect(emojify('\u2757')).toEqual(
|
expect(emojify('\u2757')).toEqual(
|
||||||
'<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture>');
|
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does multiple unicode', () => {
|
it('does multiple unicode', () => {
|
||||||
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
|
expect(emojify('\u2757 #\uFE0F\u20E3')).toEqual(
|
||||||
'<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture>');
|
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
|
||||||
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
|
expect(emojify('\u2757#\uFE0F\u20E3')).toEqual(
|
||||||
'<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture><picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture>');
|
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg">');
|
||||||
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
|
expect(emojify('\u2757 #\uFE0F\u20E3 \u2757')).toEqual(
|
||||||
'<picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture> <picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture>');
|
'<img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg">');
|
||||||
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
|
expect(emojify('foo \u2757 #\uFE0F\u20E3 bar')).toEqual(
|
||||||
'foo <picture><img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"></picture> <picture><img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"></picture> bar');
|
'foo <img draggable="false" class="emojione" alt="❗" title=":exclamation:" src="/emoji/2757.svg"> <img draggable="false" class="emojione" alt="#️⃣" title=":hash:" src="/emoji/23-20e3.svg"> bar');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores unicode inside of tags', () => {
|
it('ignores unicode inside of tags', () => {
|
||||||
@@ -46,16 +46,16 @@ describe('emoji', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('does multiple emoji properly (issue 5188)', () => {
|
it('does multiple emoji properly (issue 5188)', () => {
|
||||||
expect(emojify('👌🌈💕')).toEqual('<picture><img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"></picture><picture><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"></picture><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture>');
|
expect(emojify('👌🌈💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
|
||||||
expect(emojify('👌 🌈 💕')).toEqual('<picture><img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"></picture> <picture><img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"></picture> <picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture>');
|
expect(emojify('👌 🌈 💕')).toEqual('<img draggable="false" class="emojione" alt="👌" title=":ok_hand:" src="/emoji/1f44c.svg"> <img draggable="false" class="emojione" alt="🌈" title=":rainbow:" src="/emoji/1f308.svg"> <img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does an emoji that has no shortcode', () => {
|
it('does an emoji that has no shortcode', () => {
|
||||||
expect(emojify('👁🗨')).toEqual('<picture><img draggable="false" class="emojione" alt="👁🗨" title="" src="/emoji/1f441-200d-1f5e8.svg"></picture>');
|
expect(emojify('👁🗨')).toEqual('<img draggable="false" class="emojione" alt="👁🗨" title="" src="/emoji/1f441-200d-1f5e8.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does an emoji whose filename is irregular', () => {
|
it('does an emoji whose filename is irregular', () => {
|
||||||
expect(emojify('↙️')).toEqual('<picture><img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg"></picture>');
|
expect(emojify('↙️')).toEqual('<img draggable="false" class="emojione" alt="↙️" title=":arrow_lower_left:" src="/emoji/2199.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('avoid emojifying on invisible text', () => {
|
it('avoid emojifying on invisible text', () => {
|
||||||
@@ -67,11 +67,11 @@ describe('emoji', () => {
|
|||||||
|
|
||||||
it('avoid emojifying on invisible text with nested tags', () => {
|
it('avoid emojifying on invisible text with nested tags', () => {
|
||||||
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
|
expect(emojify('<span class="invisible">😄<span class="foo">bar</span>😴</span>😇'))
|
||||||
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
|
.toEqual('<span class="invisible">😄<span class="foo">bar</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
|
||||||
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
|
expect(emojify('<span class="invisible">😄<span class="invisible">😕</span>😴</span>😇'))
|
||||||
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
|
.toEqual('<span class="invisible">😄<span class="invisible">😕</span>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
|
||||||
expect(emojify('<span class="invisible">😄<br>😴</span>😇'))
|
expect(emojify('<span class="invisible">😄<br>😴</span>😇'))
|
||||||
.toEqual('<span class="invisible">😄<br>😴</span><picture><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg"></picture>');
|
.toEqual('<span class="invisible">😄<br>😴</span><img draggable="false" class="emojione" alt="😇" title=":innocent:" src="/emoji/1f607.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not emojify emojis with textual presentation VS15 character', () => {
|
it('does not emojify emojis with textual presentation VS15 character', () => {
|
||||||
@@ -81,17 +81,17 @@ describe('emoji', () => {
|
|||||||
|
|
||||||
it('does a simple emoji properly', () => {
|
it('does a simple emoji properly', () => {
|
||||||
expect(emojify('♀♂'))
|
expect(emojify('♀♂'))
|
||||||
.toEqual('<picture><img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg"></picture><picture><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg"></picture>');
|
.toEqual('<img draggable="false" class="emojione" alt="♀" title=":female_sign:" src="/emoji/2640.svg"><img draggable="false" class="emojione" alt="♂" title=":male_sign:" src="/emoji/2642.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does an emoji containing ZWJ properly', () => {
|
it('does an emoji containing ZWJ properly', () => {
|
||||||
expect(emojify('💂♀️💂♂️'))
|
expect(emojify('💂♀️💂♂️'))
|
||||||
.toEqual('<picture><img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"></picture><picture><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg"></picture>');
|
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg">');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps ordering as expected (issue fixed by PR 20677)', () => {
|
it('keeps ordering as expected (issue fixed by PR 20677)', () => {
|
||||||
expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>'))
|
expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener" target="_blank">#<span>foo</span></a> test: foo.</p>'))
|
||||||
.toEqual('<p><picture><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"></picture> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>');
|
.toEqual('<p><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener" target="_blank">#<span>foo</span></a> test: foo.</p>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -97,30 +97,30 @@ const emojifyTextNode = (node, customEmojis) => {
|
|||||||
const { filename, shortCode } = unicodeMapping[unicode_emoji];
|
const { filename, shortCode } = unicodeMapping[unicode_emoji];
|
||||||
const title = shortCode ? `:${shortCode}:` : '';
|
const title = shortCode ? `:${shortCode}:` : '';
|
||||||
|
|
||||||
replacement = document.createElement('picture');
|
|
||||||
|
|
||||||
const isSystemTheme = !!document.body?.classList.contains('theme-system');
|
const isSystemTheme = !!document.body?.classList.contains('theme-system');
|
||||||
|
|
||||||
if(isSystemTheme) {
|
const theme = (isSystemTheme || document.body?.classList.contains('theme-mastodon-light')) ? 'light' : 'dark';
|
||||||
let source = document.createElement('source');
|
|
||||||
source.setAttribute('media', '(prefers-color-scheme: dark)');
|
|
||||||
source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, "dark")}.svg`);
|
|
||||||
replacement.appendChild(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
let img = document.createElement('img');
|
const imageFilename = emojiFilename(filename, theme);
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
img.setAttribute('draggable', 'false');
|
img.setAttribute('draggable', 'false');
|
||||||
img.setAttribute('class', 'emojione');
|
img.setAttribute('class', 'emojione');
|
||||||
img.setAttribute('alt', unicode_emoji);
|
img.setAttribute('alt', unicode_emoji);
|
||||||
img.setAttribute('title', title);
|
img.setAttribute('title', title);
|
||||||
|
img.setAttribute('src', `${assetHost}/emoji/${imageFilename}.svg`);
|
||||||
|
|
||||||
let theme = "light";
|
if (isSystemTheme && imageFilename !== emojiFilename(filename, 'dark')) {
|
||||||
|
replacement = document.createElement('picture');
|
||||||
|
|
||||||
if(!isSystemTheme && !document.body?.classList.contains('theme-mastodon-light'))
|
const source = document.createElement('source');
|
||||||
theme = "dark";
|
source.setAttribute('media', '(prefers-color-scheme: dark)');
|
||||||
|
source.setAttribute('srcset', `${assetHost}/emoji/${emojiFilename(filename, 'dark')}.svg`);
|
||||||
img.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename, theme)}.svg`);
|
replacement.appendChild(source);
|
||||||
replacement.appendChild(img);
|
replacement.appendChild(img);
|
||||||
|
} else {
|
||||||
|
replacement = img;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the processed-up-to-now string and the emoji replacement
|
// Add the processed-up-to-now string and the emoji replacement
|
||||||
@@ -135,7 +135,7 @@ const emojifyTextNode = (node, customEmojis) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const emojifyNode = (node, customEmojis) => {
|
const emojifyNode = (node, customEmojis) => {
|
||||||
for (const child of node.childNodes) {
|
for (const child of Array.from(node.childNodes)) {
|
||||||
switch(child.nodeType) {
|
switch(child.nodeType) {
|
||||||
case Node.TEXT_NODE:
|
case Node.TEXT_NODE:
|
||||||
emojifyTextNode(child, customEmojis);
|
emojifyTextNode(child, customEmojis);
|
||||||
|
|||||||
@@ -129,8 +129,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
|
||||||
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
|
||||||
|
setCanScrollRight(bodyRef.current.scrollLeft < 0);
|
||||||
|
} else {
|
||||||
|
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
||||||
|
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
||||||
|
}
|
||||||
}, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
|
}, [setCanScrollRight, setCanScrollLeft, bodyRef, suggestions]);
|
||||||
|
|
||||||
const handleLeftNav = useCallback(() => {
|
const handleLeftNav = useCallback(() => {
|
||||||
@@ -146,8 +151,13 @@ export const InlineFollowSuggestions = ({ hidden }) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
if (getComputedStyle(bodyRef.current).direction === 'rtl') {
|
||||||
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
setCanScrollLeft((bodyRef.current.clientWidth - bodyRef.current.scrollLeft) < bodyRef.current.scrollWidth);
|
||||||
|
setCanScrollRight(bodyRef.current.scrollLeft < 0);
|
||||||
|
} else {
|
||||||
|
setCanScrollLeft(bodyRef.current.scrollLeft > 0);
|
||||||
|
setCanScrollRight((bodyRef.current.scrollLeft + bodyRef.current.clientWidth) < bodyRef.current.scrollWidth);
|
||||||
|
}
|
||||||
}, [setCanScrollRight, setCanScrollLeft, bodyRef]);
|
}, [setCanScrollRight, setCanScrollLeft, bodyRef]);
|
||||||
|
|
||||||
const handleDismiss = useCallback(() => {
|
const handleDismiss = useCallback(() => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
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 +26,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 +51,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 +77,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}
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export const DetailedStatus: React.FC<{
|
|||||||
media = <PictureInPicturePlaceholder aspectRatio={attachmentAspectRatio} />;
|
media = <PictureInPicturePlaceholder aspectRatio={attachmentAspectRatio} />;
|
||||||
} else if (status.get('media_attachments').size > 0) {
|
} else if (status.get('media_attachments').size > 0) {
|
||||||
if (
|
if (
|
||||||
['image', 'gifv'].includes(
|
['image', 'gifv', 'unknown'].includes(
|
||||||
status.getIn(['media_attachments', 0, 'type']) as string,
|
status.getIn(['media_attachments', 0, 'type']) as string,
|
||||||
) ||
|
) ||
|
||||||
status.get('media_attachments').size > 1
|
status.get('media_attachments').size > 1
|
||||||
@@ -219,12 +219,12 @@ export const DetailedStatus: React.FC<{
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (status.get('spoiler_text').length === 0) {
|
} else if (status.get('card')) {
|
||||||
media = (
|
media = (
|
||||||
<Card
|
<Card
|
||||||
sensitive={status.get('sensitive')}
|
sensitive={status.get('sensitive')}
|
||||||
onOpenMedia={onOpenMedia}
|
onOpenMedia={onOpenMedia}
|
||||||
card={status.get('card', null)}
|
card={status.get('card')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
"alert.rate_limited.title": "معدل الطلبات محدود",
|
"alert.rate_limited.title": "معدل الطلبات محدود",
|
||||||
"alert.unexpected.message": "لقد طرأ خطأ غير متوقّع.",
|
"alert.unexpected.message": "لقد طرأ خطأ غير متوقّع.",
|
||||||
"alert.unexpected.title": "المعذرة!",
|
"alert.unexpected.title": "المعذرة!",
|
||||||
|
"alt_text_badge.title": "نص بديل",
|
||||||
"announcement.announcement": "إعلان",
|
"announcement.announcement": "إعلان",
|
||||||
"attachments_list.unprocessed": "(غير معالَج)",
|
"attachments_list.unprocessed": "(غير معالَج)",
|
||||||
"audio.hide": "إخفاء المقطع الصوتي",
|
"audio.hide": "إخفاء المقطع الصوتي",
|
||||||
@@ -195,6 +196,7 @@
|
|||||||
"confirmations.unfollow.title": "إلغاء متابعة المستخدم؟",
|
"confirmations.unfollow.title": "إلغاء متابعة المستخدم؟",
|
||||||
"content_warning.hide": "إخفاء المنشور",
|
"content_warning.hide": "إخفاء المنشور",
|
||||||
"content_warning.show": "إظهار على أي حال",
|
"content_warning.show": "إظهار على أي حال",
|
||||||
|
"content_warning.show_more": "إظهار المزيد",
|
||||||
"conversation.delete": "احذف المحادثة",
|
"conversation.delete": "احذف المحادثة",
|
||||||
"conversation.mark_as_read": "اعتبرها كمقروءة",
|
"conversation.mark_as_read": "اعتبرها كمقروءة",
|
||||||
"conversation.open": "اعرض المحادثة",
|
"conversation.open": "اعرض المحادثة",
|
||||||
@@ -301,7 +303,6 @@
|
|||||||
"filter_modal.select_filter.subtitle": "استخدم فئة موجودة أو قم بإنشاء فئة جديدة",
|
"filter_modal.select_filter.subtitle": "استخدم فئة موجودة أو قم بإنشاء فئة جديدة",
|
||||||
"filter_modal.select_filter.title": "تصفية هذا المنشور",
|
"filter_modal.select_filter.title": "تصفية هذا المنشور",
|
||||||
"filter_modal.title.status": "تصفية منشور",
|
"filter_modal.title.status": "تصفية منشور",
|
||||||
"filter_warning.matches_filter": "يطابق عامل التصفية \"{title}\"",
|
|
||||||
"filtered_notifications_banner.title": "الإشعارات المصفاة",
|
"filtered_notifications_banner.title": "الإشعارات المصفاة",
|
||||||
"firehose.all": "الكل",
|
"firehose.all": "الكل",
|
||||||
"firehose.local": "هذا الخادم",
|
"firehose.local": "هذا الخادم",
|
||||||
@@ -490,6 +491,7 @@
|
|||||||
"notification.label.private_reply": "رد خاص",
|
"notification.label.private_reply": "رد خاص",
|
||||||
"notification.label.reply": "ردّ",
|
"notification.label.reply": "ردّ",
|
||||||
"notification.mention": "إشارة",
|
"notification.mention": "إشارة",
|
||||||
|
"notification.mentioned_you": "أشارَ إليك {name}",
|
||||||
"notification.moderation-warning.learn_more": "اعرف المزيد",
|
"notification.moderation-warning.learn_more": "اعرف المزيد",
|
||||||
"notification.moderation_warning": "لقد تلقيت تحذيرًا بالإشراف",
|
"notification.moderation_warning": "لقد تلقيت تحذيرًا بالإشراف",
|
||||||
"notification.moderation_warning.action_delete_statuses": "تم حذف بعض من منشوراتك.",
|
"notification.moderation_warning.action_delete_statuses": "تم حذف بعض من منشوراتك.",
|
||||||
@@ -758,7 +760,7 @@
|
|||||||
"status.history.edited": "عدله {name} {date}",
|
"status.history.edited": "عدله {name} {date}",
|
||||||
"status.load_more": "حمّل المزيد",
|
"status.load_more": "حمّل المزيد",
|
||||||
"status.media.open": "اضغط للفتح",
|
"status.media.open": "اضغط للفتح",
|
||||||
"status.media.show": "اضغط لإظهاره",
|
"status.media.show": "اضغط لإظهارها",
|
||||||
"status.media_hidden": "وسائط مخفية",
|
"status.media_hidden": "وسائط مخفية",
|
||||||
"status.mention": "أذكُر @{name}",
|
"status.mention": "أذكُر @{name}",
|
||||||
"status.more": "المزيد",
|
"status.more": "المزيد",
|
||||||
|
|||||||
@@ -4,32 +4,38 @@
|
|||||||
"about.disclaimer": "Mastodon ye software gratuito y de códigu llibre, y una marca rexistrada de Mastodon gGmbH.",
|
"about.disclaimer": "Mastodon ye software gratuito y de códigu llibre, y una marca rexistrada de Mastodon gGmbH.",
|
||||||
"about.domain_blocks.no_reason_available": "El motivu nun ta disponible",
|
"about.domain_blocks.no_reason_available": "El motivu nun ta disponible",
|
||||||
"about.domain_blocks.preamble": "Polo xeneral, Mastodon permítete ver el conteníu ya interactuar colos perfiles d'otros sirvidores nel fediversu. Estes son les esceiciones que se ficieron nesti sirvidor.",
|
"about.domain_blocks.preamble": "Polo xeneral, Mastodon permítete ver el conteníu ya interactuar colos perfiles d'otros sirvidores nel fediversu. Estes son les esceiciones que se ficieron nesti sirvidor.",
|
||||||
"about.domain_blocks.silenced.explanation": "Polo xeneral, nun ves los perfiles ya'l conteníu d'esti sirvidor sacante que los busques o decidas siguilos.",
|
"about.domain_blocks.silenced.explanation": "Polo xeneral, nun ves los perfiles y el conteníu d'esti sirvidor sacante que los busques o decidas siguilos.",
|
||||||
"about.domain_blocks.silenced.title": "Llendóse",
|
"about.domain_blocks.silenced.title": "Llendóse",
|
||||||
"about.domain_blocks.suspended.explanation": "Nun se procesa, atroxa nin intercambia nengún datu d'esti sirvidor, lo que fai que cualesquier interaición o comunicación colos sos perfiles seya imposible.",
|
"about.domain_blocks.suspended.explanation": "Nun se procesa, atroxa nin intercambia nengún datu d'esti sirvidor, lo que fai imposible cualesquier interaición o comunicación colos sos perfiles.",
|
||||||
"about.domain_blocks.suspended.title": "Suspendióse",
|
"about.domain_blocks.suspended.title": "Suspendióse",
|
||||||
"about.not_available": "Esta información nun ta disponible nesti sirvidor.",
|
"about.not_available": "Esta información nun ta disponible nesti sirvidor.",
|
||||||
"about.powered_by": "Una rede social descentralizada que tien la teunoloxía de {mastodon}",
|
"about.powered_by": "Una rede social descentralizada que tien la teunoloxía de {mastodon}",
|
||||||
"about.rules": "Normes del sirvidor",
|
"about.rules": "Normes del sirvidor",
|
||||||
"account.account_note_header": "Nota personal",
|
"account.account_note_header": "Nota personal",
|
||||||
"account.add_or_remove_from_list": "Amestar o quitar de les llistes",
|
"account.add_or_remove_from_list": "Amestar o quitar de les llistes",
|
||||||
|
"account.badges.bot": "Automatizóse",
|
||||||
"account.badges.group": "Grupu",
|
"account.badges.group": "Grupu",
|
||||||
"account.block": "Bloquiar a @{name}",
|
"account.block": "Bloquiar a @{name}",
|
||||||
"account.block_domain": "Bloquiar el dominiu {domain}",
|
"account.block_domain": "Bloquiar el dominiu {domain}",
|
||||||
|
"account.block_short": "Bloquiar",
|
||||||
"account.blocked": "Perfil bloquiáu",
|
"account.blocked": "Perfil bloquiáu",
|
||||||
|
"account.copy": "Copiar l'enlllaz al perfil",
|
||||||
"account.direct": "Mentar a @{name} per privao",
|
"account.direct": "Mentar a @{name} per privao",
|
||||||
"account.disable_notifications": "Dexar d'avisame cuando @{name} espublice artículos",
|
"account.disable_notifications": "Dexar d'avisame cuando @{name} espublice artículos",
|
||||||
"account.domain_blocked": "Dominiu bloquiáu",
|
"account.domain_blocked": "Dominiu bloquiáu",
|
||||||
"account.edit_profile": "Editar el perfil",
|
"account.edit_profile": "Editar el perfil",
|
||||||
"account.enable_notifications": "Avisame cuando @{name} espublice artículos",
|
"account.enable_notifications": "Avisame cuando @{name} espublice artículos",
|
||||||
"account.endorse": "Destacar nel perfil",
|
"account.endorse": "Destacar nel perfil",
|
||||||
"account.featured_tags.last_status_never": "Nun hai nengún artículu",
|
"account.featured_tags.last_status_never": "Nun hai nenguna publicación",
|
||||||
"account.featured_tags.title": "Etiquetes destacaes de: {name}",
|
"account.featured_tags.title": "Etiquetes destacaes de: {name}",
|
||||||
"account.follow": "Siguir",
|
"account.follow": "Siguir",
|
||||||
|
"account.follow_back": "Siguir tamién",
|
||||||
"account.followers": "Siguidores",
|
"account.followers": "Siguidores",
|
||||||
"account.followers.empty": "Naide sigue a esti perfil.",
|
"account.followers.empty": "Naide sigue a esti perfil.",
|
||||||
|
"account.following": "Siguiendo",
|
||||||
"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.go_to_profile": "Dir al perfil",
|
||||||
|
"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}",
|
||||||
@@ -37,6 +43,8 @@
|
|||||||
"account.mention": "Mentar a @{name}",
|
"account.mention": "Mentar a @{name}",
|
||||||
"account.moved_to": "{name} indicó qu'agora la so cuenta nueva ye:",
|
"account.moved_to": "{name} indicó qu'agora la so cuenta nueva ye:",
|
||||||
"account.mute": "Desactivar los avisos de @{name}",
|
"account.mute": "Desactivar los avisos de @{name}",
|
||||||
|
"account.mute_notifications_short": "Silenciar avisos",
|
||||||
|
"account.mute_short": "Silenciar",
|
||||||
"account.no_bio": "Nun se fornió nenguna descripción.",
|
"account.no_bio": "Nun se fornió nenguna descripción.",
|
||||||
"account.open_original_page": "Abrir la páxina orixinal",
|
"account.open_original_page": "Abrir la páxina orixinal",
|
||||||
"account.posts": "Artículos",
|
"account.posts": "Artículos",
|
||||||
@@ -47,9 +55,11 @@
|
|||||||
"account.show_reblogs": "Amosar los artículos compartíos de @{name}",
|
"account.show_reblogs": "Amosar los artículos compartíos de @{name}",
|
||||||
"account.unblock": "Desbloquiar a @{name}",
|
"account.unblock": "Desbloquiar a @{name}",
|
||||||
"account.unblock_domain": "Desbloquiar el dominiu «{domain}»",
|
"account.unblock_domain": "Desbloquiar el dominiu «{domain}»",
|
||||||
|
"account.unblock_short": "Desbloquiar",
|
||||||
"account.unendorse": "Dexar de destacar nel perfil",
|
"account.unendorse": "Dexar de destacar nel perfil",
|
||||||
"account.unfollow": "Dexar de siguir",
|
"account.unfollow": "Dexar de siguir",
|
||||||
"account.unmute": "Activar los avisos de @{name}",
|
"account.unmute": "Activar los avisos de @{name}",
|
||||||
|
"account.unmute_notifications_short": "Dexar de silenciar notificaciones",
|
||||||
"account.unmute_short": "Activar los avisos",
|
"account.unmute_short": "Activar los avisos",
|
||||||
"account_note.placeholder": "Calca equí p'amestar una nota",
|
"account_note.placeholder": "Calca equí p'amestar una nota",
|
||||||
"admin.dashboard.retention.average": "Media",
|
"admin.dashboard.retention.average": "Media",
|
||||||
@@ -58,15 +68,25 @@
|
|||||||
"alert.rate_limited.message": "Volvi tentalo dempués de la hora: {retry_time, time, medium}.",
|
"alert.rate_limited.message": "Volvi tentalo dempués de la hora: {retry_time, time, medium}.",
|
||||||
"alert.unexpected.message": "Prodúxose un error inesperáu.",
|
"alert.unexpected.message": "Prodúxose un error inesperáu.",
|
||||||
"alert.unexpected.title": "¡Meca!",
|
"alert.unexpected.title": "¡Meca!",
|
||||||
|
"alt_text_badge.title": "Testu alternativu",
|
||||||
"announcement.announcement": "Anunciu",
|
"announcement.announcement": "Anunciu",
|
||||||
"attachments_list.unprocessed": "(ensin procesar)",
|
"attachments_list.unprocessed": "(ensin procesar)",
|
||||||
|
"block_modal.show_less": "Amosar menos",
|
||||||
|
"block_modal.show_more": "Amosar más",
|
||||||
|
"block_modal.they_cant_mention": "Nun van poder mencionate o siguite.",
|
||||||
|
"block_modal.they_cant_see_posts": "Nun pueden ver les tos espublizaciones y tu nun podrás ver les suyes.",
|
||||||
|
"block_modal.you_wont_see_mentions": "Nun verás espublizaciones que-yos mencionen.",
|
||||||
"bundle_column_error.error.body": "La páxina solicitada nun se pudo renderizar. Ye posible que seya pola mor d'un fallu nel códigu o por un problema de compatibilidá del restolador.",
|
"bundle_column_error.error.body": "La páxina solicitada nun se pudo renderizar. Ye posible que seya pola mor d'un fallu nel códigu o por un problema de compatibilidá del restolador.",
|
||||||
"bundle_column_error.error.title": "¡Oh, non!",
|
"bundle_column_error.error.title": "¡Oh, non!",
|
||||||
"bundle_column_error.network.body": "Hebo un error al tentar de cargar esta páxina. Esto pudo ser pola mor d'un problema temporal cola conexón a internet o con esti sirvidor.",
|
"bundle_column_error.network.body": "Hebo un error al tentar de cargar esta páxina. Esto pudo ser pola mor d'un problema temporal cola conexón a internet o con esti sirvidor.",
|
||||||
|
"bundle_column_error.network.title": "Fallu de rede",
|
||||||
|
"bundle_column_error.retry": "Retentar",
|
||||||
"bundle_column_error.return": "Volver al aniciu",
|
"bundle_column_error.return": "Volver al aniciu",
|
||||||
"bundle_column_error.routing.body": "Nun se pudo atopar la páxina solicitada. ¿De xuru que la URL de la barra de direiciones ta bien escrita?",
|
"bundle_column_error.routing.body": "Nun se pudo atopar la páxina solicitada. ¿De xuru que la URL de la barra de direiciones ta bien escrita?",
|
||||||
"bundle_column_error.routing.title": "404",
|
"bundle_column_error.routing.title": "404",
|
||||||
|
"bundle_modal_error.close": "Zarrar",
|
||||||
"bundle_modal_error.message": "Asocedió daqué malo mentanto se cargaba esti componente.",
|
"bundle_modal_error.message": "Asocedió daqué malo mentanto se cargaba esti componente.",
|
||||||
|
"bundle_modal_error.retry": "Retentar",
|
||||||
"closed_registrations.other_server_instructions": "Darréu que Mastodon ye una rede social descentralizada, pues crear una cuenta n'otru sirvidor y siguir interactuando con esti.",
|
"closed_registrations.other_server_instructions": "Darréu que Mastodon ye una rede social descentralizada, pues crear una cuenta n'otru sirvidor y siguir interactuando con esti.",
|
||||||
"closed_registrations_modal.description": "Anguaño nun ye posible crear cuentes en {domain}, mas ten en cuenta que nun precises una cuenta nesti sirvidor pa usar Mastodon.",
|
"closed_registrations_modal.description": "Anguaño nun ye posible crear cuentes en {domain}, mas ten en cuenta que nun precises una cuenta nesti sirvidor pa usar Mastodon.",
|
||||||
"closed_registrations_modal.find_another_server": "Atopar otru sirvidor",
|
"closed_registrations_modal.find_another_server": "Atopar otru sirvidor",
|
||||||
@@ -78,6 +98,8 @@
|
|||||||
"column.community": "Llinia de tiempu llocal",
|
"column.community": "Llinia de tiempu llocal",
|
||||||
"column.direct": "Menciones privaes",
|
"column.direct": "Menciones privaes",
|
||||||
"column.domain_blocks": "Dominios bloquiaos",
|
"column.domain_blocks": "Dominios bloquiaos",
|
||||||
|
"column.favourites": "Favoritos",
|
||||||
|
"column.firehose": "Feed en direuto",
|
||||||
"column.follow_requests": "Solicitúes de siguimientu",
|
"column.follow_requests": "Solicitúes de siguimientu",
|
||||||
"column.home": "Aniciu",
|
"column.home": "Aniciu",
|
||||||
"column.lists": "Llistes",
|
"column.lists": "Llistes",
|
||||||
@@ -96,7 +118,9 @@
|
|||||||
"community.column_settings.remote_only": "Namás lo remoto",
|
"community.column_settings.remote_only": "Namás lo remoto",
|
||||||
"compose.language.change": "Camudar la llingua",
|
"compose.language.change": "Camudar la llingua",
|
||||||
"compose.language.search": "Buscar llingües…",
|
"compose.language.search": "Buscar llingües…",
|
||||||
"compose.published.body": "Espublizóse l'artículu.",
|
"compose.published.body": "Publicóse la publicación.",
|
||||||
|
"compose.published.open": "Abrir",
|
||||||
|
"compose.saved.body": "Guardóse la publicación.",
|
||||||
"compose_form.direct_message_warning_learn_more": "Saber más",
|
"compose_form.direct_message_warning_learn_more": "Saber más",
|
||||||
"compose_form.encryption_warning": "Los artículos de Mastodon nun tán cifraos de puntu a puntu. Nun compartas nengún tipu d'información sensible per Mastodon.",
|
"compose_form.encryption_warning": "Los artículos de Mastodon nun tán cifraos de puntu a puntu. Nun compartas nengún tipu d'información sensible per Mastodon.",
|
||||||
"compose_form.lock_disclaimer": "La to cuenta nun ye {locked}. Cualesquier perfil pue siguite pa ver los artículos que son namás pa siguidores.",
|
"compose_form.lock_disclaimer": "La to cuenta nun ye {locked}. Cualesquier perfil pue siguite pa ver los artículos que son namás pa siguidores.",
|
||||||
@@ -104,33 +128,55 @@
|
|||||||
"compose_form.placeholder": "¿En qué pienses?",
|
"compose_form.placeholder": "¿En qué pienses?",
|
||||||
"compose_form.poll.option_placeholder": "Opción {number}",
|
"compose_form.poll.option_placeholder": "Opción {number}",
|
||||||
"compose_form.poll.type": "Tipu",
|
"compose_form.poll.type": "Tipu",
|
||||||
"compose_form.publish_form": "Artículu nuevu",
|
"compose_form.publish": "Espublizar",
|
||||||
|
"compose_form.publish_form": "Publicación nueva",
|
||||||
|
"compose_form.reply": "Responder",
|
||||||
"confirmation_modal.cancel": "Encaboxar",
|
"confirmation_modal.cancel": "Encaboxar",
|
||||||
"confirmations.block.confirm": "Bloquiar",
|
"confirmations.block.confirm": "Bloquiar",
|
||||||
"confirmations.delete.confirm": "Desaniciar",
|
"confirmations.delete.confirm": "Desaniciar",
|
||||||
"confirmations.delete.message": "¿De xuru que quies desaniciar esti artículu?",
|
"confirmations.delete.message": "¿De xuru que quies desaniciar esta publicación?",
|
||||||
|
"confirmations.delete.title": "¿Quies desaniciar esta publicación?",
|
||||||
"confirmations.delete_list.confirm": "Desaniciar",
|
"confirmations.delete_list.confirm": "Desaniciar",
|
||||||
|
"confirmations.delete_list.message": "¿De xuru que quies desaniciar permanentemente esta llista?",
|
||||||
|
"confirmations.delete_list.title": "¿Quies desaniciar la llista?",
|
||||||
"confirmations.discard_edit_media.confirm": "Escartar",
|
"confirmations.discard_edit_media.confirm": "Escartar",
|
||||||
|
"confirmations.edit.confirm": "Editar",
|
||||||
"confirmations.edit.message": "La edición va sobrescribir el mensaxe que tas escribiendo. ¿De xuru que quies siguir?",
|
"confirmations.edit.message": "La edición va sobrescribir el mensaxe que tas escribiendo. ¿De xuru que quies siguir?",
|
||||||
"confirmations.logout.confirm": "Zarrar la sesión",
|
"confirmations.logout.confirm": "Zarrar la sesión",
|
||||||
"confirmations.logout.message": "¿De xuru que quies zarrar la sesión?",
|
"confirmations.logout.message": "¿De xuru que quies zarrar la sesión?",
|
||||||
|
"confirmations.logout.title": "¿Quies zarrar la sesión?",
|
||||||
"confirmations.redraft.confirm": "Desaniciar y reeditar",
|
"confirmations.redraft.confirm": "Desaniciar y reeditar",
|
||||||
|
"confirmations.redraft.title": "¿Desaniciar y reeditar la publicación?",
|
||||||
"confirmations.reply.confirm": "Responder",
|
"confirmations.reply.confirm": "Responder",
|
||||||
|
"confirmations.reply.message": "Responder agora va sobrescribir el mensaxe que tas componiendo anguaño. ¿De xuru que quies siguir?",
|
||||||
"confirmations.unfollow.confirm": "Dexar de siguir",
|
"confirmations.unfollow.confirm": "Dexar de siguir",
|
||||||
"confirmations.unfollow.message": "¿De xuru que quies dexar de siguir a {name}?",
|
"confirmations.unfollow.message": "¿De xuru que quies dexar de siguir a {name}?",
|
||||||
|
"confirmations.unfollow.title": "¿Dexar de siguir al usuariu?",
|
||||||
|
"content_warning.hide": "Esconder la publicación",
|
||||||
|
"content_warning.show": "Amosar de toes toes",
|
||||||
|
"content_warning.show_more": "Amosar más",
|
||||||
"conversation.delete": "Desaniciar la conversación",
|
"conversation.delete": "Desaniciar la conversación",
|
||||||
|
"conversation.mark_as_read": "Marcar como lleíu",
|
||||||
"conversation.open": "Ver la conversación",
|
"conversation.open": "Ver la conversación",
|
||||||
"conversation.with": "Con {names}",
|
"conversation.with": "Con {names}",
|
||||||
|
"copy_icon_button.copied": "Copiáu nel cartafueyu",
|
||||||
"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",
|
||||||
"directory.recently_active": "Con actividá recién",
|
"directory.recently_active": "Con actividá recién",
|
||||||
|
"disabled_account_banner.account_settings": "Axustes de la cuenta",
|
||||||
"dismissable_banner.community_timeline": "Esta seición contién los artículos públicos más actuales de los perfiles agospiaos nel dominiu {domain}.",
|
"dismissable_banner.community_timeline": "Esta seición contién los artículos públicos más actuales de los perfiles agospiaos nel dominiu {domain}.",
|
||||||
"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.",
|
"domain_block_modal.block": "Bloquiar el sirvidor",
|
||||||
|
"domain_block_modal.they_cant_follow": "Naide d'esti sirvidor pue siguite.",
|
||||||
|
"domain_block_modal.title": "Bloquiar el dominiu?",
|
||||||
|
"domain_pill.server": "Sirvidor",
|
||||||
|
"domain_pill.username": "Nome d'usuariu",
|
||||||
|
"embed.instructions": "Empotra esta publicación 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",
|
||||||
@@ -144,9 +190,10 @@
|
|||||||
"emoji_button.search_results": "Resultaos de la busca",
|
"emoji_button.search_results": "Resultaos de la busca",
|
||||||
"emoji_button.symbols": "Símbolos",
|
"emoji_button.symbols": "Símbolos",
|
||||||
"emoji_button.travel": "Viaxes y llugares",
|
"emoji_button.travel": "Viaxes y llugares",
|
||||||
"empty_column.account_timeline": "¡Equí nun hai nengún artículu!",
|
"empty_column.account_suspended": "Cuenta suspendida",
|
||||||
|
"empty_column.account_timeline": "¡Equí nun hai nenguna publicación!",
|
||||||
"empty_column.blocks": "Nun bloquiesti a nengún perfil.",
|
"empty_column.blocks": "Nun bloquiesti a nengún perfil.",
|
||||||
"empty_column.bookmarked_statuses": "Nun tienes nengún artículu en Marcadores. Cuando amiestes dalgún, apaez equí.",
|
"empty_column.bookmarked_statuses": "Nun tienes nenguna publicación en Marcadores. Cuando amiestes dalguna, va apaecer equí.",
|
||||||
"empty_column.direct": "Nun tienes nenguna mención privada. Cuando unvies o recibas dalguna, apaez equí.",
|
"empty_column.direct": "Nun tienes nenguna mención privada. Cuando unvies o recibas dalguna, apaez equí.",
|
||||||
"empty_column.domain_blocks": "Nun hai nengún dominiu bloquiáu.",
|
"empty_column.domain_blocks": "Nun hai nengún dominiu bloquiáu.",
|
||||||
"empty_column.explore_statuses": "Agora nun hai nada en tendencia. ¡Volvi equí dempués!",
|
"empty_column.explore_statuses": "Agora nun hai nada en tendencia. ¡Volvi equí dempués!",
|
||||||
@@ -168,20 +215,21 @@
|
|||||||
"explore.trending_links": "Noticies",
|
"explore.trending_links": "Noticies",
|
||||||
"explore.trending_statuses": "Artículos",
|
"explore.trending_statuses": "Artículos",
|
||||||
"explore.trending_tags": "Etiquetes",
|
"explore.trending_tags": "Etiquetes",
|
||||||
"filter_modal.added.context_mismatch_explanation": "Esta categoría de peñera nun s'aplica al contestu nel qu'accediesti a esti artículu. Si tamién quies que se peñere l'artículu nesti contestu, tienes d'editar la peñera.",
|
"filter_modal.added.context_mismatch_explanation": "Esta categoría de peñera nun s'aplica al contestu nel qu'accediesti a esta publicación. Si tamién quies que se peñere la publicación nesti contestu, tienes d'editar la peñera.",
|
||||||
"filter_modal.added.context_mismatch_title": "¡El contestu nun coincide!",
|
"filter_modal.added.context_mismatch_title": "¡El contestu nun coincide!",
|
||||||
"filter_modal.added.expired_explanation": "Esta categoría de peñera caducó, tienes de camudar la so data de caducidá p'aplicala.",
|
"filter_modal.added.expired_explanation": "Esta categoría de peñera caducó, tienes de camudar la so data de caducidá p'aplicala.",
|
||||||
"filter_modal.added.expired_title": "¡La peñera caducó!",
|
"filter_modal.added.expired_title": "¡La peñera caducó!",
|
||||||
"filter_modal.added.review_and_configure": "Pa revisar y configurar a fondu esta categoría de peñera, vete a la {settings_link}.",
|
"filter_modal.added.review_and_configure": "Pa revisar y configurar a fondu esta categoría de peñera, vete a la {settings_link}.",
|
||||||
"filter_modal.added.review_and_configure_title": "Configuración de la peñera",
|
"filter_modal.added.review_and_configure_title": "Configuración de la peñera",
|
||||||
"filter_modal.added.settings_link": "páxina de configuración",
|
"filter_modal.added.settings_link": "páxina de configuración",
|
||||||
"filter_modal.added.short_explanation": "Esti artículu amestóse a la categoría de peñera siguiente: {title}.",
|
"filter_modal.added.short_explanation": "Esta publicación amestóse a la categoría de peñera siguiente: {title}.",
|
||||||
"filter_modal.added.title": "¡Amestóse la peñera!",
|
"filter_modal.added.title": "¡Amestóse la peñera!",
|
||||||
|
"filter_modal.select_filter.expired": "caducó",
|
||||||
"filter_modal.select_filter.prompt_new": "Categoría nueva: {name}",
|
"filter_modal.select_filter.prompt_new": "Categoría nueva: {name}",
|
||||||
"filter_modal.select_filter.search": "Buscar o crear",
|
"filter_modal.select_filter.search": "Buscar o crear",
|
||||||
"filter_modal.select_filter.subtitle": "Usa una categoría esistente o créala",
|
"filter_modal.select_filter.subtitle": "Usa una categoría esistente o créala",
|
||||||
"filter_modal.select_filter.title": "Peñerar esti artículu",
|
"filter_modal.select_filter.title": "Peñerar esta publicación",
|
||||||
"filter_modal.title.status": "Peñera d'un artículu",
|
"filter_modal.title.status": "Peñera d'una publicación",
|
||||||
"firehose.all": "Tolos sirvidores",
|
"firehose.all": "Tolos sirvidores",
|
||||||
"firehose.local": "Esti sirvidor",
|
"firehose.local": "Esti sirvidor",
|
||||||
"firehose.remote": "Otros sirvidores",
|
"firehose.remote": "Otros sirvidores",
|
||||||
@@ -196,6 +244,7 @@
|
|||||||
"follow_suggestions.similar_to_recently_followed_longer": "Aseméyase a los perfiles que siguiesti apocayá",
|
"follow_suggestions.similar_to_recently_followed_longer": "Aseméyase a los perfiles que siguiesti apocayá",
|
||||||
"follow_suggestions.view_all": "Ver too",
|
"follow_suggestions.view_all": "Ver too",
|
||||||
"follow_suggestions.who_to_follow": "A quién siguir",
|
"follow_suggestions.who_to_follow": "A quién siguir",
|
||||||
|
"followed_tags": "Etiquetes siguíes",
|
||||||
"footer.about": "Tocante a",
|
"footer.about": "Tocante a",
|
||||||
"footer.directory": "Direutoriu de perfiles",
|
"footer.directory": "Direutoriu de perfiles",
|
||||||
"footer.get_app": "Consiguir l'aplicación",
|
"footer.get_app": "Consiguir l'aplicación",
|
||||||
@@ -210,30 +259,37 @@
|
|||||||
"hashtag.column_header.tag_mode.any": "o {additional}",
|
"hashtag.column_header.tag_mode.any": "o {additional}",
|
||||||
"hashtag.column_header.tag_mode.none": "ensin {additional}",
|
"hashtag.column_header.tag_mode.none": "ensin {additional}",
|
||||||
"hashtag.column_settings.select.no_options_message": "Nun s'atopó nenguna suxerencia",
|
"hashtag.column_settings.select.no_options_message": "Nun s'atopó nenguna suxerencia",
|
||||||
|
"hashtag.column_settings.select.placeholder": "Introduz etiquetes…",
|
||||||
|
"hashtag.column_settings.tag_mode.all": "Toes estes",
|
||||||
|
"hashtag.column_settings.tag_mode.any": "Cualesquiera d'estes",
|
||||||
|
"hashtag.column_settings.tag_mode.none": "Nenguna d'estes",
|
||||||
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}",
|
"hashtag.counter_by_accounts": "{count, plural, one {{counter} participante} other {{counter} participantes}}",
|
||||||
"hashtag.follow": "Siguir a la etiqueta",
|
"hashtag.follow": "Siguir a la etiqueta",
|
||||||
"hashtag.unfollow": "Dexar de siguir a la etiqueta",
|
"hashtag.unfollow": "Dexar de siguir a la etiqueta",
|
||||||
|
"hints.threads.replies_may_be_missing": "Ye posible que falten les rempuestes d'otros sirvidores.",
|
||||||
"home.column_settings.show_reblogs": "Amosar los artículos compartíos",
|
"home.column_settings.show_reblogs": "Amosar los artículos compartíos",
|
||||||
"home.column_settings.show_replies": "Amosar les rempuestes",
|
"home.column_settings.show_replies": "Amosar les rempuestes",
|
||||||
"home.pending_critical_update.body": "¡Anueva'l sirvidor de Mastodon namás que puedas!",
|
"home.pending_critical_update.body": "¡Anueva'l sirvidor de Mastodon namás que puedas!",
|
||||||
|
"home.show_announcements": "Amosar anuncios",
|
||||||
"interaction_modal.description.follow": "Con una cuenta de Mastodon, pues siguir a {name} pa recibir los artículos de so nel to feed d'aniciu.",
|
"interaction_modal.description.follow": "Con una cuenta de Mastodon, pues siguir a {name} pa recibir los artículos de so nel to feed d'aniciu.",
|
||||||
"interaction_modal.description.reblog": "Con una cuenta de Mastodon, pues compartir esti artículu colos perfiles que te sigan.",
|
"interaction_modal.description.reblog": "Con una cuenta de Mastodon, pues compartir esta publicación colos perfiles que te sigan.",
|
||||||
"interaction_modal.description.reply": "Con una cuenta de Mastodon, pues responder a esti artículu.",
|
"interaction_modal.description.reply": "Con una cuenta de Mastodon, pues responder a esta publicación.",
|
||||||
"interaction_modal.on_another_server": "N'otru sirvidor",
|
"interaction_modal.on_another_server": "N'otru sirvidor",
|
||||||
"interaction_modal.on_this_server": "Nesti sirvidor",
|
"interaction_modal.on_this_server": "Nesti sirvidor",
|
||||||
"interaction_modal.title.reply": "Rempuesta al artículu de: {name}",
|
"interaction_modal.title.follow": "Siguir a {name}",
|
||||||
|
"interaction_modal.title.reply": "Rempuesta a la publicación de: {name}",
|
||||||
"intervals.full.days": "{number, plural, one {# día} other {# díes}}",
|
"intervals.full.days": "{number, plural, one {# día} other {# díes}}",
|
||||||
"intervals.full.hours": "{number, plural, one {# hora} other {# hores}}",
|
"intervals.full.hours": "{number, plural, one {# hora} other {# hores}}",
|
||||||
"intervals.full.minutes": "{number, plural, one {# minutu} other {# minutos}}",
|
"intervals.full.minutes": "{number, plural, one {# minutu} other {# minutos}}",
|
||||||
"keyboard_shortcuts.back": "Dir p'atrás",
|
"keyboard_shortcuts.back": "Dir p'atrás",
|
||||||
"keyboard_shortcuts.blocked": "Abrir la llista de perfiles bloquiaos",
|
"keyboard_shortcuts.blocked": "Abrir la llista de perfiles bloquiaos",
|
||||||
"keyboard_shortcuts.boost": "Compartir un artículu",
|
"keyboard_shortcuts.boost": "Compartir una publicación",
|
||||||
"keyboard_shortcuts.column": "Enfocar una columna",
|
"keyboard_shortcuts.column": "Enfocar una columna",
|
||||||
"keyboard_shortcuts.compose": "Enfocar l'área de composición",
|
"keyboard_shortcuts.compose": "Enfocar l'área de composición",
|
||||||
"keyboard_shortcuts.description": "Descripción",
|
"keyboard_shortcuts.description": "Descripción",
|
||||||
"keyboard_shortcuts.direct": "p'abrir la columna de les menciones privaes",
|
"keyboard_shortcuts.direct": "p'abrir la columna de les menciones privaes",
|
||||||
"keyboard_shortcuts.down": "Baxar na llista",
|
"keyboard_shortcuts.down": "Baxar na llista",
|
||||||
"keyboard_shortcuts.enter": "Abrir un artículu",
|
"keyboard_shortcuts.enter": "Abrir una publicación",
|
||||||
"keyboard_shortcuts.federated": "Abrir la llinia de tiempu federada",
|
"keyboard_shortcuts.federated": "Abrir la llinia de tiempu federada",
|
||||||
"keyboard_shortcuts.heading": "Atayos del tecláu",
|
"keyboard_shortcuts.heading": "Atayos del tecláu",
|
||||||
"keyboard_shortcuts.home": "Abrir la llinia de tiempu del aniciu",
|
"keyboard_shortcuts.home": "Abrir la llinia de tiempu del aniciu",
|
||||||
@@ -247,15 +303,21 @@
|
|||||||
"keyboard_shortcuts.open_media": "Abrir el conteníu mutimedia",
|
"keyboard_shortcuts.open_media": "Abrir el conteníu mutimedia",
|
||||||
"keyboard_shortcuts.pinned": "Abrir la llista d'artículos fixaos",
|
"keyboard_shortcuts.pinned": "Abrir la llista d'artículos fixaos",
|
||||||
"keyboard_shortcuts.profile": "Abrir el perfil del autor/a",
|
"keyboard_shortcuts.profile": "Abrir el perfil del autor/a",
|
||||||
"keyboard_shortcuts.reply": "Responder a un artículu",
|
"keyboard_shortcuts.reply": "Responder a una publicación",
|
||||||
"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": "Escribir una publicación nueva",
|
||||||
"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",
|
||||||
|
"lightbox.close": "Zarrar",
|
||||||
|
"lightbox.next": "Siguiente",
|
||||||
"limited_account_hint.action": "Amosar el perfil de toes toes",
|
"limited_account_hint.action": "Amosar el perfil de toes toes",
|
||||||
|
"link_preview.author": "Por {name}",
|
||||||
|
"link_preview.more_from_author": "Más de {name}",
|
||||||
|
"lists.account.add": "Amestar a la llista",
|
||||||
|
"lists.account.remove": "Desaniciar de la llista",
|
||||||
"lists.delete": "Desaniciar la llista",
|
"lists.delete": "Desaniciar la llista",
|
||||||
"lists.edit": "Editar la llista",
|
"lists.edit": "Editar la llista",
|
||||||
"lists.edit.submit": "Camudar el títulu",
|
"lists.edit.submit": "Camudar el títulu",
|
||||||
@@ -268,6 +330,7 @@
|
|||||||
"lists.search": "Buscar ente los perfiles que sigues",
|
"lists.search": "Buscar ente los perfiles que sigues",
|
||||||
"lists.subheading": "Les tos llistes",
|
"lists.subheading": "Les tos llistes",
|
||||||
"load_pending": "{count, plural, one {# elementu nuevu} other {# elementos nuevos}}",
|
"load_pending": "{count, plural, one {# elementu nuevu} other {# elementos nuevos}}",
|
||||||
|
"loading_indicator.label": "Cargando…",
|
||||||
"navigation_bar.about": "Tocante a",
|
"navigation_bar.about": "Tocante a",
|
||||||
"navigation_bar.blocks": "Perfiles bloquiaos",
|
"navigation_bar.blocks": "Perfiles bloquiaos",
|
||||||
"navigation_bar.bookmarks": "Marcadores",
|
"navigation_bar.bookmarks": "Marcadores",
|
||||||
@@ -275,13 +338,17 @@
|
|||||||
"navigation_bar.direct": "Menciones privaes",
|
"navigation_bar.direct": "Menciones privaes",
|
||||||
"navigation_bar.domain_blocks": "Dominios bloquiaos",
|
"navigation_bar.domain_blocks": "Dominios bloquiaos",
|
||||||
"navigation_bar.explore": "Esploración",
|
"navigation_bar.explore": "Esploración",
|
||||||
|
"navigation_bar.favourites": "Favoritos",
|
||||||
"navigation_bar.filters": "Pallabres desactivaes",
|
"navigation_bar.filters": "Pallabres desactivaes",
|
||||||
"navigation_bar.follow_requests": "Solicitúes de siguimientu",
|
"navigation_bar.follow_requests": "Solicitúes de siguimientu",
|
||||||
|
"navigation_bar.followed_tags": "Etiquetes siguíes",
|
||||||
"navigation_bar.follows_and_followers": "Perfiles que sigues y te siguen",
|
"navigation_bar.follows_and_followers": "Perfiles que sigues y te siguen",
|
||||||
"navigation_bar.lists": "Llistes",
|
"navigation_bar.lists": "Llistes",
|
||||||
"navigation_bar.logout": "Zarrar la sesión",
|
"navigation_bar.logout": "Zarrar la sesión",
|
||||||
|
"navigation_bar.moderation": "Moderación",
|
||||||
"navigation_bar.mutes": "Perfiles colos avisos desactivaos",
|
"navigation_bar.mutes": "Perfiles colos avisos desactivaos",
|
||||||
"navigation_bar.opened_in_classic_interface": "Los artículos, les cuentes y otres páxines específiques ábrense por defeutu na interfaz web clásica.",
|
"navigation_bar.opened_in_classic_interface": "Los artículos, les cuentes y otres páxines específiques ábrense por defeutu na interfaz web clásica.",
|
||||||
|
"navigation_bar.personal": "Personal",
|
||||||
"navigation_bar.pins": "Artículos fixaos",
|
"navigation_bar.pins": "Artículos fixaos",
|
||||||
"navigation_bar.preferences": "Preferencies",
|
"navigation_bar.preferences": "Preferencies",
|
||||||
"navigation_bar.public_timeline": "Llinia de tiempu federada",
|
"navigation_bar.public_timeline": "Llinia de tiempu federada",
|
||||||
@@ -291,14 +358,27 @@
|
|||||||
"notification.admin.sign_up": "{name} rexistróse",
|
"notification.admin.sign_up": "{name} rexistróse",
|
||||||
"notification.follow": "{name} siguióte",
|
"notification.follow": "{name} siguióte",
|
||||||
"notification.follow_request": "{name} solicitó siguite",
|
"notification.follow_request": "{name} solicitó siguite",
|
||||||
"notification.reblog": "{name} compartió'l to artículu",
|
"notification.label.mention": "Mención",
|
||||||
|
"notification.label.private_mention": "Mención privada",
|
||||||
|
"notification.label.private_reply": "Rempuesta privada",
|
||||||
|
"notification.label.reply": "Responder",
|
||||||
|
"notification.mention": "Mención",
|
||||||
|
"notification.mentioned_you": "{name} mentóte",
|
||||||
|
"notification.moderation-warning.learn_more": "Deprender más",
|
||||||
|
"notification.poll": "Finó una encuesta na que votesti",
|
||||||
|
"notification.reblog": "{name} compartió la to publicación",
|
||||||
"notification.status": "{name} ta acabante d'espublizar",
|
"notification.status": "{name} ta acabante d'espublizar",
|
||||||
"notification.update": "{name} editó un artículu",
|
"notification.update": "{name} editó una publicación",
|
||||||
|
"notification_requests.edit_selection": "Editar",
|
||||||
|
"notification_requests.exit_selection": "Fecho",
|
||||||
"notifications.clear": "Borrar los avisos",
|
"notifications.clear": "Borrar los avisos",
|
||||||
"notifications.column_settings.admin.report": "Informes nuevos:",
|
"notifications.column_settings.admin.report": "Informes nuevos:",
|
||||||
"notifications.column_settings.admin.sign_up": "Rexistros nuevos:",
|
"notifications.column_settings.admin.sign_up": "Rexistros nuevos:",
|
||||||
|
"notifications.column_settings.favourite": "Favoritos:",
|
||||||
|
"notifications.column_settings.filter_bar.advanced": "Amosar toles categoríes",
|
||||||
"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:",
|
||||||
@@ -308,10 +388,16 @@
|
|||||||
"notifications.column_settings.unread_notifications.category": "Avisos ensin lleer",
|
"notifications.column_settings.unread_notifications.category": "Avisos ensin lleer",
|
||||||
"notifications.column_settings.unread_notifications.highlight": "Rescamplar los avisos ensin lleer",
|
"notifications.column_settings.unread_notifications.highlight": "Rescamplar los avisos ensin lleer",
|
||||||
"notifications.column_settings.update": "Ediciones:",
|
"notifications.column_settings.update": "Ediciones:",
|
||||||
|
"notifications.filter.all": "Too",
|
||||||
|
"notifications.filter.favourites": "Favoritos",
|
||||||
"notifications.filter.mentions": "Menciones",
|
"notifications.filter.mentions": "Menciones",
|
||||||
|
"notifications.filter.polls": "Resultaos de la encuesta",
|
||||||
"notifications.group": "{count} avisos",
|
"notifications.group": "{count} avisos",
|
||||||
"notifications.mark_as_read": "Marcar tolos avisos como lleíos",
|
"notifications.mark_as_read": "Marcar tolos avisos como lleíos",
|
||||||
"notifications.permission_required": "Los avisos d'escritoriu nun tán disponibles porque nun se concedió'l permisu riquíu.",
|
"notifications.permission_required": "Los avisos d'escritoriu nun tán disponibles porque nun se concedió'l permisu riquíu.",
|
||||||
|
"notifications.policy.accept": "Aceptar",
|
||||||
|
"notifications.policy.accept_hint": "Amosar n'avisos",
|
||||||
|
"onboarding.profile.note": "Biografía",
|
||||||
"onboarding.profile.note_hint": "Pues @mentar a otros perfiles o poner #etiquetes…",
|
"onboarding.profile.note_hint": "Pues @mentar a otros perfiles o poner #etiquetes…",
|
||||||
"onboarding.start.lead": "Yá yes parte de Mastodon, una plataforma social multimedia descentralizada onde tu y non un algoritmu, personalices la to esperiencia. Vamos presentate esti llugar social nuevu:",
|
"onboarding.start.lead": "Yá yes parte de Mastodon, una plataforma social multimedia descentralizada onde tu y non un algoritmu, personalices la to esperiencia. Vamos presentate esti llugar social nuevu:",
|
||||||
"onboarding.start.skip": "¿Nun precises ayuda pa comenzar?",
|
"onboarding.start.skip": "¿Nun precises ayuda pa comenzar?",
|
||||||
@@ -327,10 +413,10 @@
|
|||||||
"poll.votes": "{votes, plural, one {# votu} other {# votos}}",
|
"poll.votes": "{votes, plural, one {# votu} other {# votos}}",
|
||||||
"poll_button.add_poll": "Amestar una encuesta",
|
"poll_button.add_poll": "Amestar una encuesta",
|
||||||
"poll_button.remove_poll": "Quitar la encuesta",
|
"poll_button.remove_poll": "Quitar la encuesta",
|
||||||
"privacy.change": "Configurar la privacidá del artículu",
|
"privacy.change": "Configurar la privacidá de la publicación",
|
||||||
"privacy.direct.short": "Perfiles específicos",
|
"privacy.direct.short": "Perfiles específicos",
|
||||||
"privacy.private.short": "Siguidores",
|
"privacy.private.short": "Siguidores",
|
||||||
"privacy.public.short": "Artículu públicu",
|
"privacy.public.short": "Publicación pública",
|
||||||
"privacy_policy.last_updated": "Data del últimu anovamientu: {date}",
|
"privacy_policy.last_updated": "Data del últimu anovamientu: {date}",
|
||||||
"privacy_policy.title": "Política de privacidá",
|
"privacy_policy.title": "Política de privacidá",
|
||||||
"refresh": "Anovar",
|
"refresh": "Anovar",
|
||||||
@@ -347,13 +433,15 @@
|
|||||||
"relative_time.seconds": "{number} s",
|
"relative_time.seconds": "{number} s",
|
||||||
"relative_time.today": "güei",
|
"relative_time.today": "güei",
|
||||||
"reply_indicator.cancel": "Encaboxar",
|
"reply_indicator.cancel": "Encaboxar",
|
||||||
|
"reply_indicator.poll": "Encuesta",
|
||||||
"report.block": "Bloquiar",
|
"report.block": "Bloquiar",
|
||||||
|
"report.categories.legal": "Llegal",
|
||||||
"report.categories.spam": "Spam",
|
"report.categories.spam": "Spam",
|
||||||
"report.categories.violation": "El conteníu incumple una o más normes del sirvidor",
|
"report.categories.violation": "El conteníu incumple una o más normes del sirvidor",
|
||||||
"report.category.subtitle": "Escueyi la meyor opción",
|
"report.category.subtitle": "Escueyi la meyor opción",
|
||||||
"report.category.title": "Dinos qué pasa con esti {type}",
|
"report.category.title": "Dinos qué pasa con esti {type}",
|
||||||
"report.category.title_account": "perfil",
|
"report.category.title_account": "perfil",
|
||||||
"report.category.title_status": "artículu",
|
"report.category.title_status": "publicación",
|
||||||
"report.close": "Fecho",
|
"report.close": "Fecho",
|
||||||
"report.comment.title": "¿Hai daqué más qu'habríemos saber?",
|
"report.comment.title": "¿Hai daqué más qu'habríemos saber?",
|
||||||
"report.forward": "Reunviar a {target}",
|
"report.forward": "Reunviar a {target}",
|
||||||
@@ -363,6 +451,7 @@
|
|||||||
"report.placeholder": "Comentarios adicionales",
|
"report.placeholder": "Comentarios adicionales",
|
||||||
"report.reasons.dislike": "Nun me presta",
|
"report.reasons.dislike": "Nun me presta",
|
||||||
"report.reasons.dislike_description": "Nun ye daqué que quiera ver",
|
"report.reasons.dislike_description": "Nun ye daqué que quiera ver",
|
||||||
|
"report.reasons.legal": "Ye illegal",
|
||||||
"report.reasons.other": "Ye daqué más",
|
"report.reasons.other": "Ye daqué más",
|
||||||
"report.reasons.other_description": "La incidencia nun s'axusta a les demás categoríes",
|
"report.reasons.other_description": "La incidencia nun s'axusta a les demás categoríes",
|
||||||
"report.reasons.spam": "Ye spam",
|
"report.reasons.spam": "Ye spam",
|
||||||
@@ -372,7 +461,7 @@
|
|||||||
"report.rules.subtitle": "Seleiciona tolo que s'axuste",
|
"report.rules.subtitle": "Seleiciona tolo que s'axuste",
|
||||||
"report.rules.title": "¿Qué normes s'incumplen?",
|
"report.rules.title": "¿Qué normes s'incumplen?",
|
||||||
"report.statuses.subtitle": "Seleiciona tolo que s'axuste",
|
"report.statuses.subtitle": "Seleiciona tolo que s'axuste",
|
||||||
"report.statuses.title": "¿Hai dalgún artículu qu'apoye esti informe?",
|
"report.statuses.title": "¿Hai dalguna publicación qu'apoye esti informe?",
|
||||||
"report.submit": "Unviar",
|
"report.submit": "Unviar",
|
||||||
"report.target": "Informe de: {target}",
|
"report.target": "Informe de: {target}",
|
||||||
"report.thanks.take_action": "Equí tienes les opciones pa controlar qué ves en Mastodon:",
|
"report.thanks.take_action": "Equí tienes les opciones pa controlar qué ves en Mastodon:",
|
||||||
@@ -381,8 +470,11 @@
|
|||||||
"report.thanks.title_actionable": "Gracies pol informe, el casu yá ta n'investigación.",
|
"report.thanks.title_actionable": "Gracies pol informe, el casu yá ta n'investigación.",
|
||||||
"report.unfollow": "Dexar de siguir a @{name}",
|
"report.unfollow": "Dexar de siguir a @{name}",
|
||||||
"report.unfollow_explanation": "Sigues a esta cuenta. Pa dexar de ver los sos artículos nel to feed d'aniciu, dexa de siguila.",
|
"report.unfollow_explanation": "Sigues a esta cuenta. Pa dexar de ver los sos artículos nel to feed d'aniciu, dexa de siguila.",
|
||||||
"report_notification.attached_statuses": "{count, plural, one {Axuntóse {count} artículu} other {Axuntáronse {count} artículos}}",
|
"report_notification.attached_statuses": "{count, plural, one {Axuntóse {count} publicación} other {Axuntáronse {count} publicaciones}}",
|
||||||
|
"report_notification.categories.legal": "Llegal",
|
||||||
"report_notification.categories.legal_sentence": "conteníu illegal",
|
"report_notification.categories.legal_sentence": "conteníu illegal",
|
||||||
|
"report_notification.categories.spam": "Spam",
|
||||||
|
"report_notification.categories.spam_sentence": "spam",
|
||||||
"report_notification.open": "Abrir l'informe",
|
"report_notification.open": "Abrir l'informe",
|
||||||
"search.no_recent_searches": "Nun hai nenguna busca recién",
|
"search.no_recent_searches": "Nun hai nenguna busca recién",
|
||||||
"search.placeholder": "Buscar",
|
"search.placeholder": "Buscar",
|
||||||
@@ -391,6 +483,7 @@
|
|||||||
"search.quick_action.go_to_hashtag": "Dir a la etiqueta {x}",
|
"search.quick_action.go_to_hashtag": "Dir a la etiqueta {x}",
|
||||||
"search.quick_action.status_search": "Artículos que concasen con {x}",
|
"search.quick_action.status_search": "Artículos que concasen con {x}",
|
||||||
"search.search_or_paste": "Busca o apiega una URL",
|
"search.search_or_paste": "Busca o apiega una URL",
|
||||||
|
"search_popout.full_text_search_disabled_message": "Nun ta disponible nel dominiu {domain}.",
|
||||||
"search_popout.language_code": "códigu de llingua ISO",
|
"search_popout.language_code": "códigu de llingua ISO",
|
||||||
"search_popout.options": "Opciones de busca",
|
"search_popout.options": "Opciones de busca",
|
||||||
"search_popout.quick_actions": "Aiciones rápides",
|
"search_popout.quick_actions": "Aiciones rápides",
|
||||||
@@ -404,37 +497,42 @@
|
|||||||
"search_results.see_all": "Ver too",
|
"search_results.see_all": "Ver too",
|
||||||
"search_results.statuses": "Artículos",
|
"search_results.statuses": "Artículos",
|
||||||
"search_results.title": "Busca de: {q}",
|
"search_results.title": "Busca de: {q}",
|
||||||
|
"server_banner.is_one_of_many": "{domain} ye unu de los munchos sirvidores independientes de Mastodon que pues usar pa participar nel fediversu.",
|
||||||
"server_banner.server_stats": "Estadístiques del sirvidor:",
|
"server_banner.server_stats": "Estadístiques del sirvidor:",
|
||||||
"sign_in_banner.create_account": "Crear una cuenta",
|
"sign_in_banner.create_account": "Crear una cuenta",
|
||||||
|
"sign_in_banner.mastodon_is": "Mastodon ye la meyor manera de siguir al momentu qué pasa.",
|
||||||
|
"sign_in_banner.sign_in": "Aniciar la sesión",
|
||||||
"sign_in_banner.sso_redirect": "Aniciar la sesión o rexistrase",
|
"sign_in_banner.sso_redirect": "Aniciar la sesión o rexistrase",
|
||||||
"status.admin_account": "Abrir la interfaz de moderación pa @{name}",
|
"status.admin_account": "Abrir la interfaz de moderación pa @{name}",
|
||||||
"status.admin_domain": "Abrir la interfaz de moderación pa «{domain}»",
|
"status.admin_domain": "Abrir la interfaz de moderación pa «{domain}»",
|
||||||
"status.admin_status": "Abrir esti artículu na interfaz de moderación",
|
"status.admin_status": "Abrir esta publicación na interfaz de moderación",
|
||||||
"status.block": "Bloquiar a @{name}",
|
"status.block": "Bloquiar a @{name}",
|
||||||
"status.bookmark": "Meter en Marcadores",
|
"status.bookmark": "Meter en Marcadores",
|
||||||
"status.cannot_reblog": "Esti artículu nun se pue compartir",
|
"status.cannot_reblog": "Esta publicación nun se pue compartir",
|
||||||
"status.copy": "Copiar l'enllaz al artículu",
|
"status.copy": "Copiar l'enllaz a la publicación",
|
||||||
"status.delete": "Desaniciar",
|
"status.delete": "Desaniciar",
|
||||||
"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.filter": "Peñerar esti artículu",
|
"status.embed": "Consiguir el códigu pa empotrar",
|
||||||
|
"status.filter": "Peñerar esta publicación",
|
||||||
"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}",
|
||||||
"status.mute_conversation": "Desactivar los avisos de la conversación",
|
"status.mute_conversation": "Desactivar los avisos de la conversación",
|
||||||
"status.open": "Espander esti artículu",
|
"status.open": "Espander esta publicación",
|
||||||
"status.pin": "Fixar nel perfil",
|
"status.pin": "Fixar nel perfil",
|
||||||
"status.pinned": "Artículu fixáu",
|
"status.pinned": "Publicación fixada",
|
||||||
"status.read_more": "Lleer más",
|
"status.read_more": "Lleer más",
|
||||||
"status.reblog": "Compartir",
|
"status.reblog": "Compartir",
|
||||||
"status.reblogged_by": "{name} compartió",
|
"status.reblogged_by": "{name} compartió",
|
||||||
"status.reblogs.empty": "Naide nun compartió esti artículu. Cuando daquién lo faiga, apaez equí.",
|
"status.reblogs.empty": "Naide nun compartió esta publicación. Cuando daquién lo faiga, va apaecer equí.",
|
||||||
"status.redraft": "Desaniciar y reeditar",
|
"status.redraft": "Desaniciar y reeditar",
|
||||||
|
"status.remove_bookmark": "Desaniciar el marcador",
|
||||||
"status.replied_to": "En rempuesta a {name}",
|
"status.replied_to": "En rempuesta a {name}",
|
||||||
"status.reply": "Responder",
|
"status.reply": "Responder",
|
||||||
"status.replyAll": "Responder al filu",
|
"status.replyAll": "Responder al filu",
|
||||||
@@ -447,6 +545,7 @@
|
|||||||
"status.uncached_media_warning": "La previsualización nun ta disponible",
|
"status.uncached_media_warning": "La previsualización nun ta disponible",
|
||||||
"status.unmute_conversation": "Activar los avisos de la conversación",
|
"status.unmute_conversation": "Activar los avisos de la conversación",
|
||||||
"status.unpin": "Lliberar del perfil",
|
"status.unpin": "Lliberar del perfil",
|
||||||
|
"subscribed_languages.save": "Guardar los cambeos",
|
||||||
"tabs_bar.home": "Aniciu",
|
"tabs_bar.home": "Aniciu",
|
||||||
"tabs_bar.notifications": "Avisos",
|
"tabs_bar.notifications": "Avisos",
|
||||||
"time_remaining.days": "{number, plural, one {Queda # día} other {Queden # díes}}",
|
"time_remaining.days": "{number, plural, one {Queda # día} other {Queden # díes}}",
|
||||||
@@ -459,6 +558,7 @@
|
|||||||
"units.short.billion": "{count} MM",
|
"units.short.billion": "{count} MM",
|
||||||
"units.short.million": "{count} M",
|
"units.short.million": "{count} M",
|
||||||
"units.short.thousand": "{count} mil",
|
"units.short.thousand": "{count} mil",
|
||||||
|
"upload_area.title": "Arrastra y suelta pa xubir",
|
||||||
"upload_button.label": "Amestar ficheros multimedia",
|
"upload_button.label": "Amestar ficheros multimedia",
|
||||||
"upload_error.poll": "La xuba de ficheros nun ta permitida coles encuestes.",
|
"upload_error.poll": "La xuba de ficheros nun ta permitida coles encuestes.",
|
||||||
"upload_form.audio_description": "Describi'l conteníu pa persones sordes y/o ciegues",
|
"upload_form.audio_description": "Describi'l conteníu pa persones sordes y/o ciegues",
|
||||||
@@ -468,14 +568,15 @@
|
|||||||
"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.exit_fullscreen": "Colar de la pantalla completa",
|
||||||
"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",
|
||||||
|
|||||||
270
app/javascript/mastodon/locales/az.json
Normal file
270
app/javascript/mastodon/locales/az.json
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
{
|
||||||
|
"about.blocks": "Moderasiya olunan serverlər",
|
||||||
|
"about.contact": "Əlaqə:",
|
||||||
|
"about.disclaimer": "Mastodon pulsuz, açıq-mənbəli proqram təminatıdır və Mastodon gGmbH-nin əmtəə nişanıdır.",
|
||||||
|
"about.domain_blocks.no_reason_available": "Səbəb naməlumdur",
|
||||||
|
"about.domain_blocks.preamble": "Mastodon adətən fediversedəki hər hansısa bir serverdən olan məzmuna baxmaq və istifadəçilərlə qarşılıqlı əlaqədə olmaq imkanı verir. Bunlar bu serverdə edilmiş istisnalardır.",
|
||||||
|
"about.domain_blocks.silenced.explanation": "Siz bu serverdəki profilləri və məzmunu xüsusi olaraq axtarmasanız və ya izləməsəniz ümumiyyətlə görməyəcəksiniz.",
|
||||||
|
"about.domain_blocks.silenced.title": "Məhdudlaşdırılmış",
|
||||||
|
"about.domain_blocks.suspended.explanation": "Bu serverdən heç bir data emal edilməyəcək, saxlanılmayacaq və ya mübadilə edilməyəcək və bu serverdən olan istifadəçilərlə hər hansı qarşılıqlı əlaqə qeyri-mümkün olacaq.",
|
||||||
|
"about.domain_blocks.suspended.title": "Qadağa qoyulub",
|
||||||
|
"about.not_available": "Bu məlumat bu serverdə əlçatan edilməyib.",
|
||||||
|
"about.powered_by": "{mastodon} tərəfindən təchiz edilən desentralizasiya edilmiş sosial media",
|
||||||
|
"about.rules": "Server qaydaları",
|
||||||
|
"account.account_note_header": "Şəxsi qeyd",
|
||||||
|
"account.add_or_remove_from_list": "Siyahılara əlavə et və ya sil",
|
||||||
|
"account.badges.bot": "Avtomatlaşdırılmış",
|
||||||
|
"account.badges.group": "Qrup",
|
||||||
|
"account.block": "@{name} istifadəçisini blokla",
|
||||||
|
"account.block_domain": "{domain} domenini blokla",
|
||||||
|
"account.block_short": "Blok",
|
||||||
|
"account.blocked": "Bloklanıb",
|
||||||
|
"account.cancel_follow_request": "İzləməni ləğv et",
|
||||||
|
"account.copy": "Profil linkini kopyala",
|
||||||
|
"account.direct": "@{name} istifadəçisini fərdi olaraq etiketlə",
|
||||||
|
"account.disable_notifications": "@{name} paylaşım edəndə mənə bildiriş göndərməyi dayandır",
|
||||||
|
"account.domain_blocked": "Domen bloklanıb",
|
||||||
|
"account.edit_profile": "Profili redaktə et",
|
||||||
|
"account.enable_notifications": "@{name} paylaşım edəndə mənə bildiriş göndər",
|
||||||
|
"account.endorse": "Profildə seçilmişlərə əlavə et",
|
||||||
|
"account.featured_tags.last_status_at": "Son paylaşım {date} tarixində olub",
|
||||||
|
"account.featured_tags.last_status_never": "Paylaşım yoxdur",
|
||||||
|
"account.featured_tags.title": "{name} istifadəçisinin seçilmiş heşteqləri",
|
||||||
|
"account.follow": "İzlə",
|
||||||
|
"account.follow_back": "Sən də izlə",
|
||||||
|
"account.followers": "İzləyicilər",
|
||||||
|
"account.followers.empty": "Bu istifadəçini hələ ki, heç kim izləmir.",
|
||||||
|
"account.followers_counter": "{count, plural, one {{counter} izləyici} other {{counter} izləyici}}",
|
||||||
|
"account.following": "İzləyir",
|
||||||
|
"account.following_counter": "{count, plural, one {{counter} izləyir} other {{counter} izləyir}}",
|
||||||
|
"account.follows.empty": "Bu istifadəçi hələ ki, heç kimi izləmir.",
|
||||||
|
"account.go_to_profile": "Profilə get",
|
||||||
|
"account.hide_reblogs": "@{name} istifadəçisindən olan gücləndirmələri gizlət",
|
||||||
|
"account.in_memoriam": "Xatirə.",
|
||||||
|
"account.joined_short": "Qoşulub",
|
||||||
|
"account.languages": "Abunə olunmuş dilləri dəyiş",
|
||||||
|
"account.link_verified_on": "Bu linkin dəqiqliyi {date} tarixində yoxlanılıb",
|
||||||
|
"account.locked_info": "Bu hesabın məxfilik statusu kilidlənib. Hesabın sahibi onu kimin izləyə biləcəyini manual olaraq təyin edir.",
|
||||||
|
"account.media": "Media",
|
||||||
|
"account.mention": "@{name} istifadəçisini teq et",
|
||||||
|
"account.moved_to": "{name} onun yeni hesabının artıq bu olduğunu bildirdi:",
|
||||||
|
"account.mute": "@{name} istifadəçisini susdur",
|
||||||
|
"account.mute_notifications_short": "Bildirişləri səssizləşdir",
|
||||||
|
"account.mute_short": "Səssizləşdir",
|
||||||
|
"account.muted": "Səssizləşdirilib",
|
||||||
|
"account.mutual": "Ortaq",
|
||||||
|
"account.no_bio": "Təsvir göstərilməyib.",
|
||||||
|
"account.open_original_page": "Orijinal səhifəni aç",
|
||||||
|
"account.posts": "Paylaşım",
|
||||||
|
"account.posts_with_replies": "Paylaşım və cavablar",
|
||||||
|
"account.report": "@{name} istifadəçisini şikayət et",
|
||||||
|
"account.requested": "Təsdiq edilməsi gözlənilir. İzləmə sorğusunu ləğv etmək üçün kliklə",
|
||||||
|
"account.requested_follow": "{name} sizi izləmək sorğusu göndərib",
|
||||||
|
"account.share": "@{name} profilini paylaş",
|
||||||
|
"account.show_reblogs": "@{name} istifadəçisindən olan gücləndirmələri göstər",
|
||||||
|
"account.statuses_counter": "{count, plural, one {{counter} paylaşım} other {{counter} paylaşım}}",
|
||||||
|
"account.unblock": "@{name} blokunu aç",
|
||||||
|
"account.unblock_domain": "{domain} domeninin blokunu aç",
|
||||||
|
"account.unblock_short": "Bloku aç",
|
||||||
|
"account.unendorse": "Profildə seçilmişlərə əlavə etmə",
|
||||||
|
"account.unfollow": "İzləmədən çıxar",
|
||||||
|
"account.unmute": "@{name} səssizləşdirmədən çıxart",
|
||||||
|
"account.unmute_notifications_short": "Bildirişlərin səsini aç",
|
||||||
|
"account.unmute_short": "Səssizləşdirmədən çıxart",
|
||||||
|
"account_note.placeholder": "Qeyd əlavə etmək üçün kliklə",
|
||||||
|
"admin.dashboard.daily_retention": "Qeydiyyatdan sonrakı günə görə istifadəçi qalma dərəcəsi",
|
||||||
|
"admin.dashboard.monthly_retention": "Qeydiyyatdan sonrakı aya görə istifadəçi qalma dərəcəsi",
|
||||||
|
"admin.dashboard.retention.average": "Orta",
|
||||||
|
"admin.dashboard.retention.cohort": "Qeydiyyatdan keçmə ayı",
|
||||||
|
"admin.dashboard.retention.cohort_size": "Yeni istifadəçilər",
|
||||||
|
"admin.impact_report.instance_accounts": "Bunun siləcəyi istifadəçi hesabları",
|
||||||
|
"admin.impact_report.instance_followers": "İstifadəçilərimizin itirəcəyi izləyici sayı",
|
||||||
|
"admin.impact_report.instance_follows": "Onların istifadəçilərinin itirəcəyi izləyici sayı",
|
||||||
|
"admin.impact_report.title": "Təsirin xülasəsi",
|
||||||
|
"alert.rate_limited.message": "Zəhmət olmasa, {retry_time, time, medium} sonra yenidən cəhd edin.",
|
||||||
|
"alert.rate_limited.title": "Sürət limiti",
|
||||||
|
"alert.unexpected.message": "Bilinməyən bir xəta baş verdi.",
|
||||||
|
"alert.unexpected.title": "Ah!",
|
||||||
|
"alt_text_badge.title": "Alternativ mətn",
|
||||||
|
"announcement.announcement": "Elan",
|
||||||
|
"attachments_list.unprocessed": "(emal edilməyib)",
|
||||||
|
"audio.hide": "Audionu gizlət",
|
||||||
|
"block_modal.remote_users_caveat": "Biz {domain} serverindən qərarınıza hörmət etməsini xahiş edəcəyik. Bununla belə, bəzi serverlər blokları fərqli şəkildə idarə edə bildiyi üçün uyğunluğa zəmanət verilmir. İctimai paylaşımlar hələ də daxil olmayan istifadəçilərə görünə bilər.",
|
||||||
|
"block_modal.show_less": "Daha az göstər",
|
||||||
|
"block_modal.show_more": "Daha çox göstər",
|
||||||
|
"block_modal.they_cant_mention": "O səni teq edə bilməz və ya izləyə bilməz.",
|
||||||
|
"block_modal.they_cant_see_posts": "O sənin paylaşımlarını görməyəcək, sən də onun paylaşımlarını görməyəcəksən.",
|
||||||
|
"block_modal.they_will_know": "O sənin onu blokladığını görə biləcək.",
|
||||||
|
"block_modal.title": "İstifadəçi bloklansın?",
|
||||||
|
"block_modal.you_wont_see_mentions": "Onu teq edən postları görməyəcəksən.",
|
||||||
|
"boost_modal.combo": "Növbəti dəfə bunu atlamaq üçün {combo} klikləyə bilərsən",
|
||||||
|
"boost_modal.reblog": "Paylaşım gücləndirilsin?",
|
||||||
|
"boost_modal.undo_reblog": "Paylaşımın gücləndirilməsi ləğv edilsin?",
|
||||||
|
"bundle_column_error.copy_stacktrace": "Xəta hesabatını kopyala",
|
||||||
|
"bundle_column_error.error.body": "Tələb olunan səhifəni göstərmək mümkün olmadı. Bu, kodumuzdakı səhv və ya brauzer uyğunluğu problemi ilə bağlı ola bilər.",
|
||||||
|
"bundle_column_error.error.title": "Ah, yox!",
|
||||||
|
"bundle_column_error.network.body": "Bu səhifəni yükləməyə çalışarkən xəta baş verdi. Bu, internet bağlantınız və ya bu serverlə bağlı müvəqqəti problemlə əlaqədar ola bilər.",
|
||||||
|
"bundle_column_error.network.title": "Şəbəkə xətası",
|
||||||
|
"bundle_column_error.retry": "Yenidən cəhd et",
|
||||||
|
"bundle_column_error.return": "Ana səhifəyə qayıt",
|
||||||
|
"bundle_column_error.routing.body": "Tələb olunan səhifəni tapmaq mümkün olmadı. Ünvan çubuğundakı URL-nin düzgün olduğuna əminsiniz?",
|
||||||
|
"bundle_column_error.routing.title": "404",
|
||||||
|
"bundle_modal_error.close": "Bağla",
|
||||||
|
"bundle_modal_error.retry": "Yenidən cəhd et",
|
||||||
|
"closed_registrations.other_server_instructions": "Mastodon desentralizasiya edilmiş olduğu üçün başqa bir serverdə hesab yarada və hələ də bu serverdən istifadə edə bilərsiniz.",
|
||||||
|
"closed_registrations_modal.description": "{domain} serverində hesab yaratmaq hazırda mümkün deyil, lakin nəzərə alın ki, Mastodondan istifadə etmək üçün xüsusi olaraq {domain} serverində hesaba ehtiyacınız yoxdur.",
|
||||||
|
"closed_registrations_modal.find_another_server": "Başqa server tap",
|
||||||
|
"closed_registrations_modal.preamble": "Mastodon desentralizasiya edilib, ona görə də hesabınızı harada yaratmağınızdan asılı olmayaraq, siz bu serverdə hər kəsi izləyə və onunla əlaqə saxlaya biləcəksiniz. Siz hətta özünüz server aça bilərsiniz!",
|
||||||
|
"closed_registrations_modal.title": "Mastodonda qeydiyyatdan keçmək",
|
||||||
|
"column.about": "Haqqında",
|
||||||
|
"column.blocks": "Bloklanmış istifadəçilər",
|
||||||
|
"column.bookmarks": "Əlfəcinlər",
|
||||||
|
"column.community": "Lokal zaman qrafiki",
|
||||||
|
"column.direct": "Fərdi teqlər",
|
||||||
|
"column.directory": "Profillər arasında gəz",
|
||||||
|
"column.domain_blocks": "Bloklanmış domenlər",
|
||||||
|
"column.favourites": "Sevimlilər",
|
||||||
|
"column.firehose": "Canlı lentlər",
|
||||||
|
"column.follow_requests": "İzləyici sorğuları",
|
||||||
|
"column.home": "Ana səhifə",
|
||||||
|
"column.lists": "Siyahılar",
|
||||||
|
"column.mutes": "Səssizləşdirilmiş istifadəçilər",
|
||||||
|
"column.notifications": "Bildirişlər",
|
||||||
|
"column.pins": "Bərkidilmiş paylaşımlar",
|
||||||
|
"column.public": "Federasiya zaman qrafiki",
|
||||||
|
"column_back_button.label": "Geriyə",
|
||||||
|
"column_header.hide_settings": "Parametrləri gizlət",
|
||||||
|
"column_header.moveLeft_settings": "Sütunu sola köçür",
|
||||||
|
"column_header.moveRight_settings": "Sütunu sağa köçür",
|
||||||
|
"column_header.pin": "Bərkit",
|
||||||
|
"column_header.show_settings": "Parametrləri göstər",
|
||||||
|
"column_header.unpin": "Bərkitmə",
|
||||||
|
"column_subheading.settings": "Parametrlər",
|
||||||
|
"community.column_settings.local_only": "Sadəcə lokalda",
|
||||||
|
"community.column_settings.media_only": "Sadəcə media",
|
||||||
|
"community.column_settings.remote_only": "Sadəcə uzaq serverlər",
|
||||||
|
"compose.language.change": "Dili dəyiş",
|
||||||
|
"compose.language.search": "Dil axtar...",
|
||||||
|
"compose.published.body": "Paylaşıldı.",
|
||||||
|
"compose.published.open": "Bax",
|
||||||
|
"compose.saved.body": "Paylaşım yadda saxlandı.",
|
||||||
|
"compose_form.direct_message_warning_learn_more": "Ətraflı öyrən",
|
||||||
|
"compose_form.encryption_warning": "Mastodondakı paylaşımlar ucdan-uca şifrələnmir. Mastodonda heç bir həssas məlumat paylaşmayın.",
|
||||||
|
"compose_form.hashtag_warning": "Bu yazı ictimai olmadığı üçün heç bir heşteqdə göstərilməyəcək. Yalnız açıq yazılar heşteq ilə axtarıla bilər.",
|
||||||
|
"compose_form.lock_disclaimer": "Hesabınız {locked} deyil. Sadəcə izləyicilər üçün paylaşımlarınıza baxmaq üçün hər kəs sizi izləyə bilər.",
|
||||||
|
"compose_form.lock_disclaimer.lock": "kilidli",
|
||||||
|
"compose_form.placeholder": "Ağlınızdan nə keçir?",
|
||||||
|
"compose_form.poll.duration": "Sorğunun müddəti",
|
||||||
|
"compose_form.poll.multiple": "Çoxlu cavab",
|
||||||
|
"compose_form.poll.option_placeholder": "Seçim {number}",
|
||||||
|
"compose_form.poll.switch_to_multiple": "Çoxsaylı cavablara icazə vermək üçün sorğunu redaktə et",
|
||||||
|
"compose_form.poll.switch_to_single": "Tək cavaba icazə vermək üçün sorğunu redaktə et",
|
||||||
|
"compose_form.poll.type": "Stil",
|
||||||
|
"compose_form.publish": "Paylaş",
|
||||||
|
"compose_form.publish_form": "Yeni paylaşım",
|
||||||
|
"compose_form.reply": "Cavabla",
|
||||||
|
"compose_form.save_changes": "Yenilə",
|
||||||
|
"compose_form.spoiler.marked": "Məzmun xəbərdarlığını sil",
|
||||||
|
"compose_form.spoiler.unmarked": "Məzmun xəbərdarlığı əlavə et",
|
||||||
|
"compose_form.spoiler_placeholder": "Məzmun xəbərdarlığı (məcburi deyil)",
|
||||||
|
"confirmation_modal.cancel": "İmtina",
|
||||||
|
"confirmations.block.confirm": "Blokla",
|
||||||
|
"confirmations.delete.confirm": "Sil",
|
||||||
|
"confirmations.delete.message": "Bu paylaşımı silmək istədiyinizə əminsiniz?",
|
||||||
|
"confirmations.delete.title": "Paylaşım silinsin?",
|
||||||
|
"confirmations.delete_list.confirm": "Sil",
|
||||||
|
"confirmations.delete_list.message": "Bu siyahını həmişəlik silmək istədiyinizə əminsiniz?",
|
||||||
|
"confirmations.delete_list.title": "Siyahı silinsin?",
|
||||||
|
"confirmations.discard_edit_media.confirm": "Ləğv et",
|
||||||
|
"confirmations.discard_edit_media.message": "Media təsvirində və ya önizləmədə yadda saxlanmamış dəyişiklikləriniz var, ləğv edilsin?",
|
||||||
|
"confirmations.edit.confirm": "Redaktə et",
|
||||||
|
"confirmations.edit.message": "Redaktə etmək hazırda tərtib etdiyiniz mesajın üzərinə yazacaq. Davam etmək istədiyinizə əminsiniz?",
|
||||||
|
"confirmations.edit.title": "Paylaşım yenidə yazılsın?",
|
||||||
|
"confirmations.logout.confirm": "Çıxış et",
|
||||||
|
"confirmations.logout.message": "Çıxmaq istədiyinizə əminsiniz?",
|
||||||
|
"confirmations.logout.title": "Çıxış edilsin?",
|
||||||
|
"confirmations.mute.confirm": "Səssizləşdir",
|
||||||
|
"confirmations.redraft.confirm": "Sil və qaralamaya köçür",
|
||||||
|
"confirmations.redraft.message": "Bu paylaşımı silmək və qaralamaya köçürmək istədiyinizə əminsiniz? Bəyənmələr və gücləndirmələr itəcək və orijinal paylaşıma olan cavablar tənha qalacaq.",
|
||||||
|
"confirmations.redraft.title": "Paylaşım silinsin & qaralamaya köçürülsün?",
|
||||||
|
"confirmations.reply.confirm": "Cavabla",
|
||||||
|
"confirmations.reply.message": "İndi cavab vermək hal-hazırda yazdığınız mesajın üzərinə yazacaq. Davam etmək istədiyinizə əminsiniz?",
|
||||||
|
"confirmations.reply.title": "Paylaşım yenidən yazılsın?",
|
||||||
|
"confirmations.unfollow.confirm": "İzləmədən çıxar",
|
||||||
|
"confirmations.unfollow.message": "{name} izləmədən çıxmaq istədiyinizə əminsiniz?",
|
||||||
|
"confirmations.unfollow.title": "İstifadəçi izləmədən çıxarılsın?",
|
||||||
|
"content_warning.hide": "Paylaşımı gizlət",
|
||||||
|
"content_warning.show": "Yenə də göstər",
|
||||||
|
"content_warning.show_more": "Daha çox göstər",
|
||||||
|
"conversation.delete": "Söhbəti sil",
|
||||||
|
"conversation.mark_as_read": "Oxunmuş kimi qeyd et",
|
||||||
|
"conversation.open": "Söhbətə bax",
|
||||||
|
"conversation.with": "{names} ilə",
|
||||||
|
"copy_icon_button.copied": "Mübadilə buferinə köçürüldü",
|
||||||
|
"copypaste.copied": "Kopyalandı",
|
||||||
|
"copypaste.copy_to_clipboard": "Kopyala",
|
||||||
|
"directory.federated": "Bilinən fediversedən",
|
||||||
|
"directory.local": "Sadəcə {domain}",
|
||||||
|
"directory.new_arrivals": "Yeni gələnlər",
|
||||||
|
"directory.recently_active": "Bayaq aktiv olanlar",
|
||||||
|
"disabled_account_banner.account_settings": "Hesab parametrləri",
|
||||||
|
"disabled_account_banner.text": "Sizin hesabınız {disabledAccount} hal-hazırda deaktiv edilib.",
|
||||||
|
"dismissable_banner.community_timeline": "Bunlar, hesabları {domain} serverində yerləşən insanların ən son ictimai paylaşımlarıdır.",
|
||||||
|
"dismissable_banner.dismiss": "Bağla",
|
||||||
|
"domain_block_modal.block": "Serveri blokla",
|
||||||
|
"domain_block_modal.block_account_instead": "@{name} istifadəçisini blokla",
|
||||||
|
"domain_block_modal.they_can_interact_with_old_posts": "Bu serverdən olan insanlar köhnə paylaşımlarınızla əlaqə qura bilər.",
|
||||||
|
"domain_block_modal.they_cant_follow": "Bu serverdən heç kim sizi izləyə bilməz.",
|
||||||
|
"domain_block_modal.they_wont_know": "Onlar bloklandıqlarını bilməyəcəklər.",
|
||||||
|
"domain_block_modal.title": "Domen bloklansın?",
|
||||||
|
"domain_block_modal.you_will_lose_num_followers": "Siz {followersCount, plural, one {{followersCountDisplay} follower} other {{followersCountDisplay} izləyici}} və izlədiyiniz {followingCount, plural, one {{followingCountDisplay} istifadəçini} other {{followingCountDisplay} istifadəçini}} itirəcəksiniz.",
|
||||||
|
"domain_block_modal.you_will_lose_relationships": "Bu serverdən olan bütün izləyicilərinizi və izlədiklərinizi itirəcəksiniz.",
|
||||||
|
"domain_block_modal.you_wont_see_posts": "Bu serverdən olan paylaşımları və istifadəçilərdən olan bildirişləri görməyəcəksiniz.",
|
||||||
|
"domain_pill.activitypub_lets_connect": "Bu, təkcə Mastodonda deyil, həm də müxtəlif sosial tətbiqlərdə insanlarla əlaqə saxlamağa və onlarla ünsiyyət qurmağa imkan verir.",
|
||||||
|
"domain_pill.activitypub_like_language": "ActivityPub-ı Mastodonun digər sosial şəbəkələrlə danışdığı dil kimi düşünə bilərsiniz.",
|
||||||
|
"domain_pill.server": "Server",
|
||||||
|
"domain_pill.their_handle": "Tanıdıcısı:",
|
||||||
|
"domain_pill.their_server": "Onların bütün paylaşımlarının yaşadığı rəqəmsal ev.",
|
||||||
|
"domain_pill.their_username": "Serverdəki unikal identifikator. Fərqli serverlərdə eyni istifadəçi adı ilə istifadəçilər tapmaq mümkündür.",
|
||||||
|
"domain_pill.username": "İstifadəçi adı",
|
||||||
|
"domain_pill.whats_in_a_handle": "Tanıdıcı nədir?",
|
||||||
|
"domain_pill.who_they_are": "Tanıdıcılar kimin kim olduğunu və harada olduğunu bildirdiyi üçün siz <button>ActivityPub tərəfindən dəstəklənən platformaların</button> sosial şəbəkəsindəki bütün insanlarla əlaqə saxlaya bilərsiniz.",
|
||||||
|
"domain_pill.who_you_are": "Tanıdıcılar sizin kim olduğunuzu və harada olduğunuzu bildirdiyi üçün <button>ActivityPub tərəfindən dəstəklənən platformaların</button> sosial şəbəkəsindəki bütün insanlar sizlə əlaqə saxlaya bilər.",
|
||||||
|
"domain_pill.your_handle": "Tanıdıcınız:",
|
||||||
|
"domain_pill.your_server": "Bütün paylaşımlarınızın yaşadığı rəqəmsal ev. Buranı bəyənmirsiniz? İstədiyiniz vaxt serverdən köçün və izləyicilərinizi də aparın.",
|
||||||
|
"domain_pill.your_username": "Serverdəki unikal identifikatoruz. Fərqli serverlərdə eyni istifadəçi adı ilə istifadəçilər tapmaq mümkündür.",
|
||||||
|
"embed.instructions": "Aşağıdakı kodu kopyalayaraq bu postu veb-saytınıza yerləşdirin.",
|
||||||
|
"embed.preview": "Belə görünəcək:",
|
||||||
|
"emoji_button.activity": "Aktivlik",
|
||||||
|
"emoji_button.clear": "Təmizlə",
|
||||||
|
"emoji_button.custom": "Özəl",
|
||||||
|
"emoji_button.flags": "Bayraqlar",
|
||||||
|
"emoji_button.food": "Yemək və içki",
|
||||||
|
"emoji_button.label": "Emoji daxil et",
|
||||||
|
"emoji_button.nature": "Təbiət",
|
||||||
|
"emoji_button.not_found": "Uyğun emoji tapılmadı",
|
||||||
|
"emoji_button.objects": "Obyektlər",
|
||||||
|
"emoji_button.people": "İnsanlar",
|
||||||
|
"emoji_button.recent": "Tez-tez istifadə edilən",
|
||||||
|
"emoji_button.search": "Axtar...",
|
||||||
|
"emoji_button.search_results": "Axtarış nəticələri",
|
||||||
|
"emoji_button.symbols": "Simvollar",
|
||||||
|
"emoji_button.travel": "Səyahət və məkanlar",
|
||||||
|
"empty_column.account_hides_collections": "Bu istifadəçi bu məlumatı əlçatan etməməyi seçib",
|
||||||
|
"empty_column.account_suspended": "Hesab silinib",
|
||||||
|
"empty_column.account_timeline": "Heç bir paylaşım yoxdur!",
|
||||||
|
"empty_column.account_unavailable": "Profil əlçatan deyil",
|
||||||
|
"empty_column.blocks": "Hələ ki, heç bir istifadəçini bloklamamasınız.",
|
||||||
|
"empty_column.bookmarked_statuses": "Hələ ki, heç bir paylaşımı yadda saxlamamısınız. Yadda saxlayanda burada görünəcək.",
|
||||||
|
"empty_column.community": "Lokal zaman qrafiki boşdur. Topun yuvarlanmağa başlaması üçün ictimai bir şey paylaşın!",
|
||||||
|
"empty_column.direct": "Gizli etiketiniz yoxdur. Göndərdikdə və ya qəbul etdikdə burada görəcəksiniz.",
|
||||||
|
"empty_column.domain_blocks": "Hələ ki, bloklanmış domen yoxdur.",
|
||||||
|
"empty_column.explore_statuses": "Hal-hazırda trenddə heç yoxdur. Daha sonra yenidən yoxlayın!",
|
||||||
|
"empty_column.favourited_statuses": "Bəyəndiyiniz paylaşımlar yoxdur. Birini bəyəndikdə burada görünəcək.",
|
||||||
|
"empty_column.favourites": "Bu paylaşımı hələ ki, heç kim bəyənməyib. Bəyənildikdə burada görünəcək.",
|
||||||
|
"empty_column.follow_requests": "İzləmə sorğularınız yoxdur. Qəbul etdikdə burada görəcəksiniz.",
|
||||||
|
"empty_column.followed_tags": "Heç bir heşteq izləmirsiniz. İzlədikdə burada görünəcək.",
|
||||||
|
"empty_column.hashtag": "Bu heşteqdə hələ ki, heç nə yoxdur.",
|
||||||
|
"follow_suggestions.hints.friends_of_friends": "Bu profil izlədiyiniz insanlar arasında populyardır.",
|
||||||
|
"follow_suggestions.hints.most_followed": "Bu profil {domain} serverində ən çox izlənilənlərdən biridir."
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user