Compare commits

...
Author SHA1 Message Date
Hampus 6e4c055ebd feat(desktop): build macOS as a universal binary (#1603) 2026-08-15 01:25:35 +02:00
fluxer-ci[bot]andhampus-fluxer 3588090f22 chore(marketing): advance pointer eecefd5 → e16301c (#1601)
Co-authored-by: hampus-fluxer <[email protected]>
2026-08-15 00:09:55 +02:00
fluxer-ci[bot]andhampus-fluxer 237b8ddfbf chore(i18n): update public marketing catalogs (#1602)
Co-authored-by: hampus-fluxer <[email protected]>
2026-08-15 00:09:50 +02:00
Hampus 3dab64d040 fix(ci): repair release history lookup (#1600) 2026-08-14 23:47:42 +02:00
Hampus 0e4e03b879 feat: refresh marketing content and catalogs (#1599) 2026-08-14 23:42:53 +02:00
Hampus b8218f906b build: add marketing web fonts and branding assets (#1598) 2026-08-14 22:35:58 +02:00
fluxer-ci[bot]andhampus-fluxer dd6dd827b5 chore(marketing): advance pointer f6ce662 → 9926afb (#1597)
Co-authored-by: hampus-fluxer <[email protected]>
2026-08-14 22:24:44 +02:00
fluxer-ci[bot]andhampus-fluxer 7922876b81 chore(i18n): update public marketing catalogs (#1596)
Co-authored-by: hampus-fluxer <[email protected]>
2026-08-14 22:15:42 +02:00
fluxer-ci[bot]andhampus-fluxer 152f64aac7 chore(marketing): advance pointer 5297a20 → f6ce662 (#1595)
Co-authored-by: hampus-fluxer <[email protected]>
2026-08-14 21:46:22 +02:00
Hampus 08566fc244 build: extract marketing and simplify releases (#1594) 2026-08-14 21:14:13 +02:00
Hampus beb906753f fix(api): eliminate idle Postgres I/O on self-hosted instances (#1593) 2026-08-14 19:55:53 +02:00
Hampus 8a9b12e6a1 fix: resolve KLIPY media through the items endpoint (#1592) 2026-08-14 19:38:37 +02:00
Hampus 01432bc682 fix(app): rework composer layout and footer alignment (#1591) 2026-08-14 19:28:22 +02:00
Hampus d56c1e5674 fix: repair CI failures and remove slowmode reset (#1588) 2026-08-14 00:19:36 +02:00
Hampus 70509fb978 fix(app): format slowmode tooltip values (#1587) 2026-08-13 23:37:13 +02:00
387 changed files with 30630 additions and 72521 deletions

No files matched your search

-6
View File
@@ -38,11 +38,6 @@ services:
target: /workspaces/fluxer/fluxer_desktop/node_modules
volume:
nocopy: true
- type: volume
source: fluxer-marketing-node-modules
target: /workspaces/fluxer/fluxer_marketing/node_modules
volume:
nocopy: true
- type: volume
source: fluxer-admin-node-modules
target: /workspaces/fluxer/fluxer_admin/node_modules
@@ -358,7 +353,6 @@ volumes:
fluxer-api-node-modules:
fluxer-app-node-modules:
fluxer-desktop-node-modules:
fluxer-marketing-node-modules:
fluxer-admin-node-modules:
package-config-node-modules:
package-constants-node-modules:
+1 -1
View File
@@ -71,7 +71,7 @@ stage "rust: fmt" cargo fmt --all -- --check
if [ "$QUICK" -eq 0 ]; then
stage "rust: clippy (workspace)" cargo clippy --workspace --all-targets -- -D warnings
else
stage "rust: clippy (servers)" cargo clippy -p fluxer_app_proxy -p fluxer_admin -p fluxer_marketing --all-targets -- -D warnings
stage "rust: clippy (servers)" cargo clippy -p fluxer_app_proxy -p fluxer_admin --all-targets -- -D warnings
fi
stage "rust: app proxy tests" cargo test -p fluxer_app_proxy
+3 -2
View File
@@ -5,8 +5,11 @@
/.direnv/
/.fluxer/
/.git/
**/.git
**/.git/**
/.github/
/.pnpm-store/
/fluxer_marketing
**/.env
**/.env.*.local
@@ -46,8 +49,6 @@
/app-dist-output/
/artifacts/
/release-input/
/release-out/
/s3_payload/
/upload_staging/
+7
View File
@@ -0,0 +1,7 @@
/.github/CODEOWNERS @fluxerapp/developers
/.github/workflows/ @fluxerapp/developers
/fluxer_marketing @fluxerapp/developers
/.gitmodules @fluxerapp/developers
/.github/workflows/dispatch-private-marketing-build.yaml @fluxerapp/developers
/packages/i18n/marketing/ @fluxerapp/developers
/scripts/setup-private-marketing.sh @fluxerapp/developers
+18
View File
@@ -89,3 +89,21 @@ Submit translations through [Weblate](https://weblate.fluxer.tools), not through
All repository activity is governed by the [Code of Conduct](CODE_OF_CONDUCT.md).
Fluxer is distributed under the [GNU Affero General Public License, version 3.0 or later](../LICENSE). By adding a DCO sign-off, you certify that you have the right to submit the contribution under that licence.
## Private marketing project
The marketing implementation is maintained in a private repository at the `fluxer_marketing` submodule path. The public workspace, bootstrap, checks, and development stack work without initializing it.
Authorized maintainers can initialize only that submodule and install its independent dependencies:
```sh
./scripts/setup-private-marketing.sh
pnpm --dir fluxer_marketing install --frozen-lockfile
cargo metadata --locked --manifest-path fluxer_marketing/Cargo.toml
```
To run the private marketing service in the local development stack and direct application links to it, add this override to the ignored `config/env/local.env` file:
```sh
FLUXER_MARKETING_ENDPOINT=http://localhost:8088/marketing
```
+3 -1
View File
@@ -24,7 +24,9 @@ f:gateway:
- any-glob-to-any-file: fluxer_gateway/**/*
f:marketing:
- changed-files:
- any-glob-to-any-file: fluxer_marketing/**/*
- any-glob-to-any-file:
- fluxer_marketing
- packages/i18n/marketing/**/*
f:media_proxy:
- changed-files:
- any-glob-to-any-file: fluxer_media_proxy/**/*
+46 -65
View File
@@ -32,17 +32,15 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this image fragment is uploaded. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
permissions:
actions: read
contents: write
packages: write
concurrency:
group: publish-${{ inputs.image }}
cancel-in-progress: false
defaults:
run:
shell: bash
@@ -55,6 +53,8 @@ jobs:
name: resolve metadata
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
outputs:
build_version: ${{ steps.vars.outputs.build_version }}
steps:
@@ -79,6 +79,10 @@ jobs:
needs: meta
runs-on: ${{ matrix.runner }}
timeout-minutes: 75
permissions:
actions: read
contents: read
packages: write
strategy:
fail-fast: false
matrix:
@@ -96,7 +100,7 @@ jobs:
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
password: ${{ github.token }}
- uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf
with:
context: ${{ inputs.context }}
@@ -119,6 +123,9 @@ jobs:
needs: [meta, build]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
env:
@@ -132,71 +139,45 @@ jobs:
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
password: ${{ github.token }}
- name: create and push multi-arch manifest
env:
IMAGE: ghcr.io/${{ env.GHCR_OWNER }}/${{ inputs.image }}
VERSION: ${{ needs.meta.outputs.build_version }}
run: |
set -euo pipefail
docker buildx imagetools create -t "${IMAGE}:${VERSION}" \
"${IMAGE}:${VERSION}-amd64" \
"${IMAGE}:${VERSION}-arm64"
docker buildx imagetools inspect "${IMAGE}:${VERSION}"
- name: Publish GitHub release
env:
GH_TOKEN: ${{ github.token }}
SOURCE_SHA: ${{ github.sha }}
VERSION: ${{ needs.meta.outputs.build_version }}
RELEASE_BASELINE_SHA: ${{ vars.RELEASE_BASELINE_SHA }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- release
publish
--component "${{ inputs.image }}"
--build-version "${VERSION}"
--source-sha "${SOURCE_SHA}"
--previous-sha "${RELEASE_BASELINE_SHA}"
- name: Advance moving image tags
env:
IMAGE: ghcr.io/${{ env.GHCR_OWNER }}/${{ inputs.image }}
VERSION: ${{ needs.meta.outputs.build_version }}
MOVING_TAGS: ${{ inputs.moving-tags }}
run: |
set -euo pipefail
tag_args=( "-t" "${IMAGE}:${VERSION}" )
tag_args=()
IFS=',' read -ra moving <<< "${MOVING_TAGS}"
for raw in "${moving[@]}"; do
t="$(echo "$raw" | xargs)"
[ -n "$t" ] && tag_args+=( "-t" "${IMAGE}:${t}" )
tag="$(echo "$raw" | xargs)"
[ -n "$tag" ] && tag_args+=( "-t" "${IMAGE}:${tag}" )
done
docker buildx imagetools create "${tag_args[@]}" \
"${IMAGE}:${VERSION}-amd64" \
"${IMAGE}:${VERSION}-arm64"
docker buildx imagetools inspect "${IMAGE}:${VERSION}"
- name: Write GitHub release image fragment
env:
GH_TOKEN: ${{ github.token }}
IMAGE_REF: ghcr.io/${{ env.GHCR_OWNER }}/${{ inputs.image }}:${{ needs.meta.outputs.build_version }}
VERSION: ${{ needs.meta.outputs.build_version }}
MOVING_TAGS: ${{ inputs.moving-tags }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- release
publish-image
--build-version "${VERSION}"
--image "${{ inputs.image }}"
--image-ref "${IMAGE_REF}"
--moving-tags "${MOVING_TAGS}"
- name: Upload GitHub release fragment
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: release-fragment-${{ inputs.image }}
path: release-out/fragments/fluxer-release-fragment-image-${{ inputs.image }}.json
if-no-files-found: error
retention-days: 14
finalise:
name: finalise GitHub release
if: ${{ inputs['finalise-release'] }}
needs: [meta, merge]
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
with:
toolchain: "1.93.0"
- name: Download GitHub release fragments
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
pattern: release-fragment-*
path: release-out/fragments
merge-multiple: true
- name: finalise GitHub release
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ needs.meta.outputs.build_version }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- release
finalise
--build-version "${VERSION}"
if (( ${#tag_args[@]} > 0 )); then
docker buildx imagetools create "${tag_args[@]}" "${IMAGE}:${VERSION}"
fi
+1 -26
View File
@@ -9,28 +9,6 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
@@ -40,7 +18,7 @@ permissions:
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
permissions: {}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
@@ -50,11 +28,8 @@ jobs:
image:
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
uses: ./.github/workflows/_build-image.yaml
with:
image: fluxer-admin
dockerfile: fluxer_admin/Dockerfile
build-version: ${{ inputs['build-version'] }}
finalise-release: ${{ inputs['finalise-release'] != false }}
secrets: inherit
+1 -26
View File
@@ -9,28 +9,6 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
@@ -40,7 +18,7 @@ permissions:
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
permissions: {}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
@@ -50,11 +28,8 @@ jobs:
image:
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
uses: ./.github/workflows/_build-image.yaml
with:
image: fluxer-api
dockerfile: fluxer_api/Dockerfile
build-version: ${{ inputs['build-version'] }}
finalise-release: ${{ inputs['finalise-release'] != false }}
secrets: inherit
@@ -9,28 +9,6 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
@@ -40,7 +18,7 @@ permissions:
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
permissions: {}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
@@ -50,12 +28,10 @@ jobs:
build:
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
uses: ./.github/workflows/_build-image.yaml
with:
image: fluxer-app-proxy-self-hosted
dockerfile: fluxer_app_proxy/Dockerfile
build-version: ${{ inputs['build-version'] }}
finalise-release: ${{ inputs['finalise-release'] != false }}
extra-build-args: |
FLUXER_APP_PROXY_TIME_FREEZE_ENABLED=false
+31 -77
View File
@@ -9,41 +9,23 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
contents: write
packages: write
concurrency:
group: publish-fluxer-app-proxy
cancel-in-progress: false
env:
GHCR_OWNER: ${{ github.repository_owner }}
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
permissions: {}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
@@ -54,9 +36,10 @@ jobs:
meta:
name: resolve metadata
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
outputs:
build_version: ${{ steps.vars.outputs.build_version }}
steps:
@@ -79,6 +62,10 @@ jobs:
needs: meta
runs-on: blacksmith-4vcpu-ubuntu-2404
timeout-minutes: 45
permissions:
actions: read
contents: read
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
env:
@@ -117,13 +104,6 @@ jobs:
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-app-proxy
--step generate_asset_manifest
- name: Upload asset manifest handoff
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: app-proxy-assets-manifest
path: app-dist-output/dist/assets-manifest.txt
if-no-files-found: error
- name: upload assets to S3 static bucket
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
@@ -139,6 +119,10 @@ jobs:
needs: meta
runs-on: blacksmith-4vcpu-ubuntu-2404-arm
timeout-minutes: 60
permissions:
actions: read
contents: read
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
env:
@@ -172,6 +156,9 @@ jobs:
needs: [meta, build, build-arm64]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
env:
@@ -180,11 +167,6 @@ jobs:
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
with:
toolchain: "1.93.0"
- name: Download app-proxy asset manifest
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
name: app-proxy-assets-manifest
path: release-input/app-proxy
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee
with:
@@ -201,58 +183,30 @@ jobs:
echo "amd64 digest: ${amd64_digest}"
docker buildx imagetools create \
-t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:v1" \
-t "${IMAGE}:latest" \
"${IMAGE}@${amd64_digest}" \
"${IMAGE}:${VERSION}-arm64"
docker buildx imagetools inspect "${IMAGE}:${VERSION}"
- name: Write GitHub release app-proxy fragment
- name: Publish GitHub release
env:
GH_TOKEN: ${{ github.token }}
IMAGE_REF: ghcr.io/${{ env.GHCR_OWNER }}/fluxer-app-proxy:${{ needs.meta.outputs.build_version }}
SOURCE_SHA: ${{ github.sha }}
VERSION: ${{ needs.meta.outputs.build_version }}
RELEASE_BASELINE_SHA: ${{ vars.RELEASE_BASELINE_SHA }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- release
publish-app-proxy
publish
--component fluxer-app-proxy
--build-version "${VERSION}"
--image fluxer-app-proxy
--image-ref "${IMAGE_REF}"
--moving-tags "v1,latest"
--asset-manifest release-input/app-proxy/assets-manifest.txt
- name: Upload GitHub release fragment
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: release-fragment-fluxer-app-proxy
path: release-out/fragments/fluxer-release-fragment-app-proxy.json
if-no-files-found: error
retention-days: 14
--source-sha "${SOURCE_SHA}"
--previous-sha "${RELEASE_BASELINE_SHA}"
finalise:
name: finalise GitHub release
if: ${{ inputs['finalise-release'] != false }}
needs: [meta, merge]
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
- name: Advance moving image tags
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
with:
toolchain: "1.93.0"
- name: Download GitHub release fragments
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
pattern: release-fragment-*
path: release-out/fragments
merge-multiple: true
- name: finalise GitHub release
env:
GH_TOKEN: ${{ github.token }}
IMAGE: ghcr.io/${{ env.GHCR_OWNER }}/fluxer-app-proxy
VERSION: ${{ needs.meta.outputs.build_version }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- release
finalise
--build-version "${VERSION}"
docker buildx imagetools create
-t "${IMAGE}:v1"
-t "${IMAGE}:latest"
"${IMAGE}:${VERSION}"
+36 -39
View File
@@ -22,7 +22,7 @@ on:
default: ""
type: string
skip_targets:
description: Comma-separated platforms or targets to skip, such as windows, macos-arm64, linux-x64.
description: Comma-separated platforms or targets to skip, such as windows, macos, linux-x64.
required: false
default: ""
type: string
@@ -46,6 +46,8 @@ jobs:
runs-on: ubuntu-24.04-arm
environment: desktop-releases
timeout-minutes: 25
permissions:
contents: read
outputs:
version: ${{ steps.meta.outputs.version }}
pub_date: ${{ steps.meta.outputs.pub_date }}
@@ -81,6 +83,8 @@ jobs:
runs-on: ubuntu-24.04-arm
environment: desktop-releases
timeout-minutes: 25
permissions:
contents: read
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
@@ -107,6 +111,10 @@ jobs:
runs-on: ${{ matrix.os }}
environment: desktop-releases
timeout-minutes: 60
permissions:
actions: read
contents: read
id-token: write
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.matrix.outputs.matrix) }}
@@ -233,7 +241,7 @@ jobs:
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
with:
toolchain: "1.93.0"
targets: ${{ matrix.platform == 'macos' && (matrix.arch == 'arm64' && 'aarch64-apple-darwin' || 'x86_64-apple-darwin') || (matrix.arch == 'arm64' && 'aarch64-unknown-linux-gnu' || 'x86_64-unknown-linux-gnu') }}
targets: ${{ matrix.platform == 'macos' && 'aarch64-apple-darwin,x86_64-apple-darwin' || (matrix.arch == 'arm64' && 'aarch64-unknown-linux-gnu' || 'x86_64-unknown-linux-gnu') }}
- name: Install MSVC ARM64 build tools
if: matrix.platform == 'windows' && matrix.arch == 'arm64'
@@ -460,8 +468,8 @@ jobs:
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
--step prepare_artifacts_unix
- name: Normalize updater YAML (arm64)
if: matrix.arch == 'arm64'
- name: Normalize updater YAML (macOS)
if: matrix.platform == 'macos'
run: >-
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
--step normalise_updater_yaml
@@ -492,6 +500,8 @@ jobs:
runs-on: ubuntu-24.04-arm
environment: desktop-releases
timeout-minutes: 60
permissions:
contents: read
env:
CHANNEL: ${{ needs.meta.outputs.build_channel }}
DISPLAY_CHANNEL: ${{ needs.meta.outputs.channel }}
@@ -544,34 +554,14 @@ jobs:
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
--step build_summary
- name: Write GitHub release desktop fragment
env:
GH_TOKEN: ${{ github.token }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- release
publish-desktop
--build-version "${{ needs.meta.outputs.version }}"
--channel "${{ needs.meta.outputs.build_channel }}"
--test-build "${{ needs.meta.outputs.test_build }}"
--s3-prefix "${{ needs.meta.outputs.s3_prefix }}"
--payload-root s3_payload
--source-sha "${{ needs.meta.outputs.source_sha }}"
- name: Upload GitHub release fragment
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: release-fragment-desktop
path: release-out/fragments/fluxer-release-fragment-desktop-${{ needs.meta.outputs.build_channel }}.json
if-no-files-found: error
retention-days: 14
- name: Cleanup S3 handoff
if: ${{ success() }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- build-desktop
--step cleanup_handoff
finalise_release:
name: Finalise GitHub desktop release
publish_release:
name: Publish GitHub desktop release
if: ${{ !cancelled() && needs.upload.result == 'success' && needs.meta.outputs.test_build != 'true' }}
needs:
- meta
@@ -579,6 +569,8 @@ jobs:
runs-on: ubuntu-24.04-arm
environment: desktop-releases
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Checkout source
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
@@ -589,18 +581,23 @@ jobs:
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
with:
toolchain: "1.93.0"
- name: Download GitHub release fragments
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
pattern: release-fragment-*
path: release-out/fragments
merge-multiple: true
- name: Finalise GitHub desktop release
- name: Publish GitHub desktop release
env:
GH_TOKEN: ${{ github.token }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- release
finalise
--build-version "${{ needs.meta.outputs.version }}"
--source-sha "${{ needs.meta.outputs.source_sha }}"
CHANNEL: ${{ needs.meta.outputs.build_channel }}
VERSION: ${{ needs.meta.outputs.version }}
SOURCE_SHA: ${{ needs.meta.outputs.source_sha }}
RELEASE_BASELINE_SHA: ${{ vars.RELEASE_BASELINE_SHA }}
run: |
set -euo pipefail
release_args=(
release publish
--component "fluxer-desktop-${CHANNEL}"
--build-version "${VERSION}"
--source-sha "${SOURCE_SHA}"
--previous-sha "${RELEASE_BASELINE_SHA}"
)
if [[ "${CHANNEL}" == "canary" ]]; then
release_args+=(--prerelease)
fi
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- "${release_args[@]}"
+1 -26
View File
@@ -9,28 +9,6 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
@@ -40,7 +18,7 @@ permissions:
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
permissions: {}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
@@ -50,12 +28,9 @@ jobs:
image:
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
uses: ./.github/workflows/_build-image.yaml
with:
image: fluxer-docs
dockerfile: fluxer_docs/Dockerfile
context: fluxer_docs
build-version: ${{ inputs['build-version'] }}
finalise-release: ${{ inputs['finalise-release'] != false }}
secrets: inherit
+1 -26
View File
@@ -9,28 +9,6 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
@@ -40,7 +18,7 @@ permissions:
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
permissions: {}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
@@ -50,11 +28,8 @@ jobs:
image:
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
uses: ./.github/workflows/_build-image.yaml
with:
image: fluxer-gateway
dockerfile: fluxer_gateway/Dockerfile
build-version: ${{ inputs['build-version'] }}
finalise-release: ${{ inputs['finalise-release'] != false }}
secrets: inherit
+1 -26
View File
@@ -9,28 +9,6 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
@@ -40,7 +18,7 @@ permissions:
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
permissions: {}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
@@ -50,11 +28,8 @@ jobs:
image:
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
uses: ./.github/workflows/_build-image.yaml
with:
image: fluxer-gifs
dockerfile: fluxer_gifs/Dockerfile
build-version: ${{ inputs['build-version'] }}
finalise-release: ${{ inputs['finalise-release'] != false }}
secrets: inherit
-60
View File
@@ -1,60 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
name: build marketing
on:
workflow_dispatch:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
contents: write
packages: write
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
steps:
- name: approved
run: echo "Build release approved."
image:
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
uses: ./.github/workflows/_build-image.yaml
with:
image: fluxer-marketing
dockerfile: fluxer_marketing/Dockerfile
build-version: ${{ inputs['build-version'] }}
finalise-release: ${{ inputs['finalise-release'] != false }}
secrets: inherit
+1 -26
View File
@@ -9,28 +9,6 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
@@ -40,7 +18,7 @@ permissions:
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
permissions: {}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
@@ -50,11 +28,8 @@ jobs:
image:
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
uses: ./.github/workflows/_build-image.yaml
with:
image: fluxer-media-proxy
dockerfile: fluxer_media_proxy/Dockerfile
build-version: ${{ inputs['build-version'] }}
finalise-release: ${{ inputs['finalise-release'] != false }}
secrets: inherit
+1 -26
View File
@@ -9,28 +9,6 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
@@ -40,7 +18,7 @@ permissions:
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
permissions: {}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
@@ -50,11 +28,8 @@ jobs:
image:
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
uses: ./.github/workflows/_build-image.yaml
with:
image: fluxer-messages
dockerfile: fluxer_messages/Dockerfile
build-version: ${{ inputs['build-version'] }}
finalise-release: ${{ inputs['finalise-release'] != false }}
secrets: inherit
+1 -26
View File
@@ -9,28 +9,6 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
@@ -40,7 +18,7 @@ permissions:
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
permissions: {}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
@@ -50,11 +28,8 @@ jobs:
image:
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
uses: ./.github/workflows/_build-image.yaml
with:
image: fluxer-snowflakes
dockerfile: fluxer_snowflakes/Dockerfile
build-version: ${{ inputs['build-version'] }}
finalise-release: ${{ inputs['finalise-release'] != false }}
secrets: inherit
+1 -26
View File
@@ -9,28 +9,6 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
@@ -40,7 +18,7 @@ permissions:
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
permissions: {}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
@@ -50,11 +28,8 @@ jobs:
image:
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
uses: ./.github/workflows/_build-image.yaml
with:
image: fluxer-static
dockerfile: fluxer_static/Dockerfile
build-version: ${{ inputs['build-version'] }}
finalise-release: ${{ inputs['finalise-release'] != false }}
secrets: inherit
+1 -26
View File
@@ -9,28 +9,6 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
@@ -40,7 +18,7 @@ permissions:
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
permissions: {}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
@@ -50,11 +28,8 @@ jobs:
image:
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
uses: ./.github/workflows/_build-image.yaml
with:
image: fluxer-unfurl
dockerfile: fluxer_unfurl/Dockerfile
build-version: ${{ inputs['build-version'] }}
finalise-release: ${{ inputs['finalise-release'] != false }}
secrets: inherit
+1 -26
View File
@@ -9,28 +9,6 @@ on:
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
workflow_call:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
finalise-release:
description: "Publish the GitHub Release after this workflow completes. Set false when an orchestrator will finalise the release."
type: boolean
required: false
default: true
approval-required:
description: "Require the protected builds environment approval before this build runs."
type: boolean
required: false
default: true
permissions:
actions: read
@@ -40,7 +18,7 @@ permissions:
jobs:
approve:
name: approve build release
if: ${{ format('{0}', inputs['approval-required']) != 'false' }}
permissions: {}
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
@@ -50,11 +28,8 @@ jobs:
image:
needs: approve
if: ${{ !cancelled() && (needs.approve.result == 'success' || needs.approve.result == 'skipped') }}
uses: ./.github/workflows/_build-image.yaml
with:
image: fluxer-users
dockerfile: fluxer_users/Dockerfile
build-version: ${{ inputs['build-version'] }}
finalise-release: ${{ inputs['finalise-release'] != false }}
secrets: inherit
-473
View File
@@ -1,473 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
name: deploy service
on:
workflow_dispatch:
inputs:
service:
description: "Helm chart name to deploy"
type: choice
required: true
options:
- api
- app-proxy
- admin
- docs
- marketing
- media-proxy
- gateway
- messages
- search
- snowflakes
- users
- unfurl
- uploads
- worker
channel:
description: "Release channel (stable or canary)"
type: choice
required: true
options:
- stable
- canary
image-tag:
description: "Docker image tag to deploy (Fluxer CalVer: YYYY.MDD.MICRO)"
type: string
required: true
build-version:
description: "Fluxer CalVer build version to inject into runtime env vars"
type: string
required: false
default: ""
allow-rollback:
description: "Allow deploying an older image tag than the newest GHCR tag"
type: boolean
required: false
default: false
workflow_call:
inputs:
service:
description: "Helm chart name to deploy"
type: string
required: true
channel:
description: "Release channel (stable or canary)"
type: string
required: true
image-tag:
description: "Docker image tag to deploy (Fluxer CalVer: YYYY.MDD.MICRO)"
type: string
required: true
build-version:
description: "Fluxer CalVer build version to inject into runtime env vars"
type: string
required: false
default: ""
allow-rollback:
description: "Allow deploying an older image tag than the newest GHCR tag"
type: boolean
required: false
default: false
secrets:
KUBE_CONFIG:
required: true
GHCR_USERNAME:
required: false
GHCR_TOKEN:
required: false
env:
GHCR_OWNER: ${{ github.repository_owner }}
GHCR_REGISTRY: ghcr.io/${{ github.repository_owner }}
jobs:
deploy:
name: deploy ${{ inputs.service }}
runs-on: ubuntu-24.04
timeout-minutes: 60
environment: ${{ inputs.channel }}
permissions:
contents: read
packages: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: install helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310
- name: configure kubectl
shell: bash
env:
KUBE_CONFIG_B64: ${{ secrets.KUBE_CONFIG }}
run: |
mkdir -p "$HOME/.kube"
printf '%s' "$KUBE_CONFIG_B64" | base64 -d > "$HOME/.kube/config"
chmod 600 "$HOME/.kube/config"
- name: resolve helm args
id: helm
shell: bash
env:
INPUT_SERVICE: ${{ inputs.service }}
INPUT_CHANNEL: ${{ inputs.channel }}
INPUT_IMAGE_TAG: ${{ inputs['image-tag'] }}
INPUT_BUILD_VERSION: ${{ inputs['build-version'] }}
run: |
SERVICE="$INPUT_SERVICE"
CHANNEL="$INPUT_CHANNEL"
TAG="$INPUT_IMAGE_TAG"
BUILD_VERSION="$INPUT_BUILD_VERSION"
GHCR_REGISTRY="${GHCR_REGISTRY:?GHCR_REGISTRY is required}"
if [[ -z "$BUILD_VERSION" ]]; then
BUILD_VERSION="$TAG"
fi
CALVER_RE='^[1-9][0-9]{3}\.[1-9][0-9]{2,3}\.(0|[1-9][0-9]{0,5})$'
if [[ ! "$TAG" =~ $CALVER_RE ]]; then
echo "::error::image-tag must be a Fluxer CalVer tag (YYYY.MDD.MICRO). Channel tags, latest tags, and suffixed tags are not deployable."
exit 1
fi
if [[ ! "$BUILD_VERSION" =~ $CALVER_RE ]]; then
echo "::error::build-version must be a Fluxer CalVer value (YYYY.MDD.MICRO)."
exit 1
fi
CHART_DIR="./deploy/helm/${SERVICE}"
VALUES_ARGS="-f ${CHART_DIR}/values.yaml"
SETS=""
BUILD_PATHS=""
DEPLOY_IMAGE=""
SYNC_WORKER_RELEASE=""
SYNC_WORKER_CHART_DIR=""
SYNC_WORKER_VALUES_ARGS=""
SYNC_WORKER_SETS=""
case "$SERVICE" in
uploads)
if [[ "$CHANNEL" != "stable" ]]; then
echo "::error::uploads deployments are stable-only (single relay serves both channels)."
exit 1
fi
RELEASE="fluxer-uploads"
DEPLOY_IMAGE="fluxer-media-proxy"
SETS="--set-string app.name=uploads --set-string app.image=fluxer-media-proxy --set-string app.tag=${TAG} --set-string app.config=stable"
SETS="${SETS} --set-string app.build.version=${BUILD_VERSION}"
SETS="${SETS} --set-string app.build.channel=stable"
;;
api|app-proxy|admin|docs|marketing)
BASE_IMAGE="fluxer-${SERVICE}"
if [[ "$SERVICE" == "docs" && "$CHANNEL" != "stable" ]]; then
echo "::error::docs deployments are stable-only."
exit 1
fi
if [[ "$CHANNEL" == "canary" ]]; then
NAME="${SERVICE}-canary"
else
NAME="${SERVICE}"
fi
DEPLOY_IMAGE="${BASE_IMAGE}"
RELEASE="fluxer-${SERVICE}-${CHANNEL}"
VALUES_ARGS="${VALUES_ARGS} -f ${CHART_DIR}/values.${CHANNEL}.prod.yaml"
SETS="--set-string app.name=${NAME} --set-string app.image=${DEPLOY_IMAGE} --set-string app.tag=${TAG}"
SETS="${SETS} --set-string app.build.version=${BUILD_VERSION}"
SETS="${SETS} --set-string app.build.channel=${CHANNEL}"
;;
media-proxy)
if [[ "$CHANNEL" != "canary" ]]; then
echo "::error::Media-proxy deployments are only supported on the canary lane."
exit 1
fi
RELEASE="fluxer-${SERVICE}"
DEPLOY_IMAGE="fluxer-media-proxy"
VALUES_ARGS="${VALUES_ARGS} -f ${CHART_DIR}/values.prod.yaml"
SETS="--set-string mediaProxy.image=fluxer-media-proxy --set-string staticProxy.image=fluxer-media-proxy --set-string mediaProxy.tag=${TAG} --set-string staticProxy.tag=${TAG} --set mediaProxy.replicas=16 --set staticProxy.replicas=4 --set-string mediaProxy.nsfwServiceEndpoint=http://int.flx-nyc-misc1.srv.fluxer.dev:8000"
BUILD_PATHS="mediaProxy staticProxy"
;;
gateway)
if [[ "$CHANNEL" != "stable" ]]; then
echo "::error::gateway deployments are stable-only."
exit 1
fi
RELEASE="fluxer-${SERVICE}"
DEPLOY_IMAGE="fluxer-gateway"
VALUES_ARGS="${VALUES_ARGS} -f ${CHART_DIR}/values.prod.yaml"
SETS="--set-string gateway.image=${DEPLOY_IMAGE} --set-string gateway.tag=${TAG}"
BUILD_PATHS="gateway"
;;
worker)
if [[ "$CHANNEL" != "stable" ]]; then
echo "::error::Worker deployments are only supported on the stable lane."
exit 1
fi
RELEASE="fluxer-${SERVICE}"
DEPLOY_IMAGE="fluxer-api"
VALUES_ARGS="${VALUES_ARGS} -f ${CHART_DIR}/values.prod.yaml"
SETS="--set-string workerRealtime.image=fluxer-api --set-string workerUnfurl.image=fluxer-api --set-string workerLifecycle.image=fluxer-api --set-string workerBatch.image=fluxer-api --set-string workerRealtime.tag=${TAG} --set-string workerUnfurl.tag=${TAG} --set-string workerLifecycle.tag=${TAG} --set-string workerBatch.tag=${TAG}"
BUILD_PATHS="workerRealtime workerUnfurl workerLifecycle workerBatch"
;;
messages|search|snowflakes|users|unfurl)
if [[ "$CHANNEL" != "stable" ]]; then
echo "::error::Shared microservice deployments are stable-only; canary traffic selection is done by the callers."
exit 1
fi
DEPLOY_IMAGE="fluxer-${SERVICE}"
RELEASE="fluxer-${SERVICE}"
VALUES_ARGS="${VALUES_ARGS} -f ${CHART_DIR}/values.prod.yaml"
SETS="--set-string svc.image=${DEPLOY_IMAGE} --set-string svc.tag=${TAG}"
SETS="${SETS} --set-string svc.build.version=${BUILD_VERSION}"
SETS="${SETS} --set-string svc.build.channel=stable"
;;
*)
echo "::error::Unknown service chart: ${SERVICE}"
exit 1
;;
esac
for BUILD_PATH in $BUILD_PATHS; do
SETS="${SETS} --set-string ${BUILD_PATH}.build.version=${BUILD_VERSION}"
SETS="${SETS} --set-string ${BUILD_PATH}.build.channel=${CHANNEL}"
done
SETS="--set-string global.registry=${GHCR_REGISTRY} ${SETS}"
if [[ "$SERVICE" == "api" && "$CHANNEL" == "canary" ]]; then
SYNC_WORKER_RELEASE="fluxer-worker"
SYNC_WORKER_CHART_DIR="./deploy/helm/worker"
SYNC_WORKER_VALUES_ARGS="-f ${SYNC_WORKER_CHART_DIR}/values.yaml -f ${SYNC_WORKER_CHART_DIR}/values.prod.yaml"
SYNC_WORKER_SETS="--set-string workerRealtime.image=fluxer-api --set-string workerUnfurl.image=fluxer-api --set-string workerLifecycle.image=fluxer-api --set-string workerBatch.image=fluxer-api"
SYNC_WORKER_SETS="${SYNC_WORKER_SETS} --set-string workerRealtime.tag=${TAG} --set-string workerUnfurl.tag=${TAG} --set-string workerLifecycle.tag=${TAG} --set-string workerBatch.tag=${TAG}"
for BUILD_PATH in workerRealtime workerUnfurl workerLifecycle workerBatch; do
SYNC_WORKER_SETS="${SYNC_WORKER_SETS} --set-string ${BUILD_PATH}.build.version=${BUILD_VERSION}"
SYNC_WORKER_SETS="${SYNC_WORKER_SETS} --set-string ${BUILD_PATH}.build.channel=${CHANNEL}"
done
SYNC_WORKER_SETS="--set-string global.registry=${GHCR_REGISTRY} ${SYNC_WORKER_SETS}"
fi
{
echo "chart-dir=${CHART_DIR}"
echo "release=${RELEASE}"
echo "values-args=${VALUES_ARGS}"
echo "sets=${SETS}"
echo "deploy-image=${DEPLOY_IMAGE}"
echo "deploy-tag=${TAG}"
echo "sync-worker-release=${SYNC_WORKER_RELEASE}"
echo "sync-worker-chart-dir=${SYNC_WORKER_CHART_DIR}"
echo "sync-worker-values-args=${SYNC_WORKER_VALUES_ARGS}"
echo "sync-worker-sets=${SYNC_WORKER_SETS}"
} >> "$GITHUB_OUTPUT"
- name: helm dependency update
shell: bash
run: |
helm dependency update "${{ steps.helm.outputs.chart-dir }}"
if [[ -n "${{ steps.helm.outputs.sync-worker-chart-dir }}" ]]; then
helm dependency update "${{ steps.helm.outputs.sync-worker-chart-dir }}"
fi
- name: prepare docker config
if: steps.helm.outputs.deploy-image != ''
shell: bash
run: |
echo "DOCKER_CONFIG=${RUNNER_TEMP}/docker-config" >> "$GITHUB_ENV"
mkdir -p "${RUNNER_TEMP}/docker-config"
- name: configure ghcr auth
if: steps.helm.outputs.deploy-image != ''
shell: bash
env:
GHCR_USERNAME: ${{ github.actor }}
GHCR_TOKEN: ${{ github.token }}
run: |
auth="$(printf '%s:%s' "$GHCR_USERNAME" "$GHCR_TOKEN" | base64 | tr -d '\n')"
printf '{"auths":{"ghcr.io":{"auth":"%s"}}}\n' "$auth" > "$DOCKER_CONFIG/config.json"
- name: verify deploy image exists
if: steps.helm.outputs.deploy-image != ''
shell: bash
run: |
IMAGE_REF="${GHCR_REGISTRY}/${{ steps.helm.outputs.deploy-image }}:${{ steps.helm.outputs.deploy-tag }}"
echo "Verifying ${IMAGE_REF}"
docker manifest inspect "${IMAGE_REF}" > /dev/null
env:
DOCKER_CLI_EXPERIMENTAL: enabled
- name: verify api deploy uses latest image
if: ${{ steps.helm.outputs.deploy-image == 'fluxer-api' && !inputs['allow-rollback'] }}
shell: bash
env:
GH_TOKEN: ${{ github.token }}
GHCR_OWNER: ${{ env.GHCR_OWNER }}
DEPLOY_TAG: ${{ steps.helm.outputs.deploy-tag }}
run: |
set -euo pipefail
CALVER_RE='^[1-9][0-9]{3}\.[1-9][0-9]{2,3}\.(0|[1-9][0-9]{0,5})$'
OWNER_TYPE="$(
curl -fsS \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"${GITHUB_API_URL:-https://api.github.com}/repos/${GITHUB_REPOSITORY}" \
| jq -r '.owner.type'
)"
case "$OWNER_TYPE" in
Organization) PACKAGE_OWNER_PATH="orgs/${GHCR_OWNER}" ;;
User) PACKAGE_OWNER_PATH="users/${GHCR_OWNER}" ;;
*)
echo "::error::Unsupported GitHub owner type for package lookup: ${OWNER_TYPE}"
exit 1
;;
esac
LATEST_TAG="$(
curl -fsS \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"${GITHUB_API_URL:-https://api.github.com}/${PACKAGE_OWNER_PATH}/packages/container/fluxer-api/versions?per_page=100" \
| jq -r --arg re "$CALVER_RE" '
[.[].metadata.container.tags[]? |
select(test($re)) |
{tag: ., parts: (split(".") | map(tonumber))}
] | max_by(.parts) | .tag // empty
'
)"
if [[ -z "$LATEST_TAG" ]]; then
echo "::error::Could not resolve the latest fluxer-api CalVer tag from GHCR."
exit 1
fi
if [[ "$DEPLOY_TAG" != "$LATEST_TAG" ]]; then
echo "::error::Refusing to deploy fluxer-api:${DEPLOY_TAG}; latest GHCR tag is fluxer-api:${LATEST_TAG}. Re-run with allow-rollback=true only for an intentional rollback."
exit 1
fi
- name: approve api image for admission policy
if: ${{ inputs.service == 'api' }}
shell: bash
env:
INPUT_CHANNEL: ${{ inputs.channel }}
run: |
DEPLOYMENT="api"
if [[ "$INPUT_CHANNEL" == "canary" ]]; then
DEPLOYMENT="api-canary"
fi
IMAGE_REF="${GHCR_REGISTRY}/${{ steps.helm.outputs.deploy-image }}:${{ steps.helm.outputs.deploy-tag }}"
PREVIOUS_IMAGE="$(kubectl -n fluxer get deployment "$DEPLOYMENT" -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null || true)"
PREVIOUS_TAG=""
if [[ -n "$PREVIOUS_IMAGE" && "$PREVIOUS_IMAGE" != "$IMAGE_REF" && "$PREVIOUS_IMAGE" == *:* ]]; then
PREVIOUS_TAG="${PREVIOUS_IMAGE##*:}"
else
PREVIOUS_IMAGE=""
fi
kubectl -n fluxer create configmap fluxer-api-approved-image \
--from-literal=tag="${{ steps.helm.outputs.deploy-tag }}" \
--from-literal=image="${IMAGE_REF}" \
--from-literal=previousTag="${PREVIOUS_TAG}" \
--from-literal=previousImage="${PREVIOUS_IMAGE}" \
--dry-run=client -o yaml \
| kubectl apply -f -
- name: ensure api admission policy
if: ${{ inputs.service == 'api' }}
shell: bash
run: kubectl apply -f deploy/k8s/fluxer-api-approved-image-policy.yaml
- name: helm upgrade
shell: bash
run: |
RELEASE="${{ steps.helm.outputs.release }}"
CHART_DIR="${{ steps.helm.outputs.chart-dir }}"
VALUES_ARGS="${{ steps.helm.outputs.values-args }}"
SETS="${{ steps.helm.outputs.sets }}"
wait_for_release_idle() {
local release="$1"
local max_checks="$2"
local check=0
local status="unknown"
while (( check < max_checks )); do
check=$((check + 1))
status=$(helm status "$release" -n fluxer -o json 2>/dev/null | jq -r '.info.status // "unknown"' || echo "unknown")
if [[ "$status" != pending-* ]]; then
echo "Release ${release} is ${status}; continuing."
return 0
fi
echo "Release ${release} is ${status}; waiting 10s (${check}/${max_checks})."
sleep 10
done
echo "::warning::Release ${release} still ${status} after ${max_checks} checks; forcing rollback."
if helm rollback "$release" -n fluxer --wait --timeout 5m 2>&1; then
echo "Rollback succeeded; continuing."
return 0
fi
echo "::error::Release ${release} is stuck in ${status} and rollback failed."
return 1
}
helm_upgrade_with_retries() {
local release="$1"
local chart_dir="$2"
local values_args="$3"
local sets="$4"
local values_args_array=()
local sets_array=()
read -r -a values_args_array <<< "$values_args"
read -r -a sets_array <<< "$sets"
wait_for_release_idle "$release" 18
local max_attempts=4
for attempt in $(seq 1 "$max_attempts"); do
echo "Running helm upgrade for ${release}, attempt ${attempt}/${max_attempts}."
set +e
upgrade_output=$(helm upgrade --install "$release" \
"$chart_dir" \
"${values_args_array[@]}" \
-n fluxer \
"${sets_array[@]}" \
--wait --timeout 20m --atomic --history-max 10 2>&1)
exit_code=$?
set -e
printf '%s\n' "$upgrade_output"
if [[ $exit_code -eq 0 ]]; then
return 0
fi
if ! grep -q "another operation (install/upgrade/rollback) is in progress" <<< "$upgrade_output"; then
return "$exit_code"
fi
if [[ $attempt -eq $max_attempts ]]; then
echo "::error::Helm upgrade failed for ${release} after ${max_attempts} attempts because another operation remained in progress."
return "$exit_code"
fi
wait_for_release_idle "$release" 18
done
}
helm_upgrade_with_retries "$RELEASE" "$CHART_DIR" "$VALUES_ARGS" "$SETS"
if [[ -n "${{ steps.helm.outputs.sync-worker-release }}" ]]; then
helm_upgrade_with_retries \
"${{ steps.helm.outputs.sync-worker-release }}" \
"${{ steps.helm.outputs.sync-worker-chart-dir }}" \
"${{ steps.helm.outputs.sync-worker-values-args }}" \
"${{ steps.helm.outputs.sync-worker-sets }}"
fi
- name: seal api admission approved image
if: ${{ success() && inputs.service == 'api' }}
shell: bash
run: |
IMAGE_REF="${GHCR_REGISTRY}/${{ steps.helm.outputs.deploy-image }}:${{ steps.helm.outputs.deploy-tag }}"
kubectl -n fluxer create configmap fluxer-api-approved-image \
--from-literal=tag="${{ steps.helm.outputs.deploy-tag }}" \
--from-literal=image="${IMAGE_REF}" \
--from-literal=previousTag="" \
--from-literal=previousImage="" \
--dry-run=client -o yaml \
| kubectl apply -f -
- name: recover stuck release on failure
if: failure() || cancelled()
shell: bash
run: |
RELEASE="${{ steps.helm.outputs.release }}"
for RELEASE in "$RELEASE" "${{ steps.helm.outputs.sync-worker-release }}"; do
if [[ -z "$RELEASE" ]]; then
continue
fi
STATUS=$(helm status "$RELEASE" -n fluxer -o json 2>/dev/null | jq -r '.info.status' 2>/dev/null || echo "unknown")
if [[ "$STATUS" == "pending-upgrade" || "$STATUS" == "pending-install" || "$STATUS" == "pending-rollback" ]]; then
echo "::warning::Release ${RELEASE} stuck in ${STATUS}, rolling back..."
helm rollback "$RELEASE" -n fluxer --wait --timeout 5m || true
fi
done
@@ -0,0 +1,220 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
name: Dispatch private marketing build
on:
push:
branches:
- main
paths:
- fluxer_marketing
- Cargo.toml
- fluxer_common/**
- packages/fonts/manifest.json
- packages/fonts/NOTICE.md
- packages/fonts/LICENSE-IBM-PLEX.txt
- packages/fonts/css/locale-fallbacks.css
- packages/fonts/files/FluxerSans/**
- packages/fonts/files/FluxerMono/**
- packages/fonts/marketing/**
- packages/i18n/marketing/**
- fluxer_static/marketing/branding/**
- .github/workflows/dispatch-private-marketing-build.yaml
permissions:
actions: read
contents: read
concurrency:
group: private-marketing-dispatch
cancel-in-progress: false
jobs:
metadata:
name: resolve exact private build metadata
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
parent_sha: ${{ steps.inputs.outputs.parent_sha }}
gitlink_sha: ${{ steps.inputs.outputs.gitlink_sha }}
build_version: ${{ steps.inputs.outputs.build_version }}
correlation_id: ${{ steps.inputs.outputs.correlation_id }}
steps:
- name: Resolve trusted build inputs
id: inputs
env:
EVENT_AFTER: ${{ github.event.after }}
GH_TOKEN: ${{ github.token }}
PARENT_SHA: ${{ github.sha }}
PUBLIC_REPOSITORY: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
set -euo pipefail
[[ "$GITHUB_EVENT_NAME" == "push" ]]
[[ "$GITHUB_REF" == "refs/heads/main" ]]
[[ "$PUBLIC_REPOSITORY" == "fluxerapp/fluxer" ]]
[[ "$PARENT_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$EVENT_AFTER" == "$PARENT_SHA" ]]
[[ "$RUN_ID" =~ ^[1-9][0-9]*$ ]]
[[ "$RUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]]
(( 10#$RUN_ATTEMPT <= 10 ))
main_sha="$(gh api "repos/$PUBLIC_REPOSITORY/git/ref/heads/main" --jq .object.sha)"
[[ "$main_sha" =~ ^[0-9a-f]{40}$ ]]
main_comparison="$(gh api "repos/$PUBLIC_REPOSITORY/compare/$PARENT_SHA...$main_sha")"
main_status="$(jq -r .status <<<"$main_comparison")"
[[ "$main_status" == "identical" || "$main_status" == "ahead" ]]
[[ "$(jq -r .merge_base_commit.sha <<<"$main_comparison")" == "$PARENT_SHA" ]]
commit="$(gh api "repos/$PUBLIC_REPOSITORY/git/commits/$PARENT_SHA")"
[[ "$(jq -r .sha <<<"$commit")" == "$PARENT_SHA" ]]
tree_sha="$(jq -r .tree.sha <<<"$commit")"
[[ "$tree_sha" =~ ^[0-9a-f]{40}$ ]]
entry="$(
gh api "repos/$PUBLIC_REPOSITORY/git/trees/$tree_sha" |
jq -cer '[.tree[] | select(.path == "fluxer_marketing")] | if length == 1 then .[0] else error("expected exactly one marketing gitlink") end'
)"
mode="$(jq -r .mode <<<"$entry")"
type="$(jq -r .type <<<"$entry")"
gitlink_sha="$(jq -r .sha <<<"$entry")"
path="$(jq -r .path <<<"$entry")"
if [[ "$mode" != "160000" || "$type" != "commit" || "$path" != "fluxer_marketing" || ! "$gitlink_sha" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::Public parent does not contain a valid fluxer_marketing gitlink."
exit 1
fi
run="$(gh api "repos/$PUBLIC_REPOSITORY/actions/runs/$RUN_ID")"
[[ "$(jq -r .id <<<"$run")" == "$RUN_ID" ]]
[[ "$(jq -r .run_attempt <<<"$run")" == "$RUN_ATTEMPT" ]]
[[ "$(jq -r .event <<<"$run")" == "push" ]]
[[ "$(jq -r .head_sha <<<"$run")" == "$PARENT_SHA" ]]
run_created_at="$(jq -r .created_at <<<"$run")"
[[ "$run_created_at" =~ ^[1-9][0-9]{3}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$ ]]
run_created_epoch="$(date -u -d "$run_created_at" +%s)"
[[ "$run_created_epoch" =~ ^[1-9][0-9]*$ ]]
build_epoch=$((run_created_epoch + 10#$RUN_ATTEMPT - 1))
read -r year month day time_segment <<<"$(date -u -d "@$build_epoch" '+%Y %m %d %H%M%S')"
month="$((10#$month))"
micro="$((10#$time_segment))"
build_version="$year.$month$day.$micro"
[[ "$build_version" =~ ^[1-9][0-9]{3}\.[1-9][0-9]{2,3}\.([0-9]|[1-9][0-9]{0,5})$ ]]
correlation_id="public-${RUN_ID}-${RUN_ATTEMPT}"
[[ "$correlation_id" =~ ^[A-Za-z0-9._:-]{1,64}$ ]]
{
echo "parent_sha=$PARENT_SHA"
echo "gitlink_sha=$gitlink_sha"
echo "build_version=$build_version"
echo "correlation_id=$correlation_id"
} >>"$GITHUB_OUTPUT"
dispatch:
name: dispatch exact private build
needs: metadata
runs-on: ubuntu-24.04
timeout-minutes: 65
environment: private-marketing-dispatch
permissions: {}
steps:
- name: Validate trusted build inputs
env:
DISPATCH_ENABLED: ${{ vars.MARKETING_DISPATCH_ENABLED }}
EXPECTED_PARENT_SHA: ${{ github.sha }}
EXPECTED_CORRELATION_ID: public-${{ github.run_id }}-${{ github.run_attempt }}
PARENT_SHA: ${{ needs.metadata.outputs.parent_sha }}
GITLINK_SHA: ${{ needs.metadata.outputs.gitlink_sha }}
BUILD_VERSION: ${{ needs.metadata.outputs.build_version }}
CORRELATION_ID: ${{ needs.metadata.outputs.correlation_id }}
run: |
set -euo pipefail
[[ "$GITHUB_EVENT_NAME" == "push" ]]
[[ "$GITHUB_REF" == "refs/heads/main" ]]
[[ "$GITHUB_REPOSITORY" == "fluxerapp/fluxer" ]]
[[ "$PARENT_SHA" == "$EXPECTED_PARENT_SHA" ]]
[[ "$PARENT_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$GITLINK_SHA" =~ ^[0-9a-f]{40}$ ]]
[[ "$BUILD_VERSION" =~ ^[1-9][0-9]{3}\.[1-9][0-9]{2,3}\.([0-9]|[1-9][0-9]{0,5})$ ]]
[[ "$CORRELATION_ID" == "$EXPECTED_CORRELATION_ID" ]]
[[ "$CORRELATION_ID" =~ ^[A-Za-z0-9._:-]{1,64}$ ]]
if [[ "$DISPATCH_ENABLED" != "true" ]]; then
echo "::error::Private marketing dispatch is intentionally disabled until the package cutover guard completes."
exit 1
fi
- name: Create private dispatch token
id: private-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
with:
client-id: ${{ vars.FLUXER_CI_APP_ID }}
private-key: ${{ secrets.FLUXER_CI_APP_KEY }}
owner: fluxerapp
repositories: marketing
permission-actions: write
- name: Dispatch exact private build
env:
GH_TOKEN: ${{ steps.private-token.outputs.token }}
PARENT_SHA: ${{ needs.metadata.outputs.parent_sha }}
GITLINK_SHA: ${{ needs.metadata.outputs.gitlink_sha }}
BUILD_VERSION: ${{ needs.metadata.outputs.build_version }}
CORRELATION_ID: ${{ needs.metadata.outputs.correlation_id }}
run: |
set -euo pipefail
gh api --method POST repos/fluxerapp/marketing/actions/workflows/build-marketing.yaml/dispatches \
--field ref=main \
--field "inputs[parent_sha]=$PARENT_SHA" \
--field "inputs[gitlink_sha]=$GITLINK_SHA" \
--field "inputs[build_version]=$BUILD_VERSION" \
--field "inputs[correlation_id]=$CORRELATION_ID"
- name: Wait for private build conclusion
env:
GH_TOKEN: ${{ steps.private-token.outputs.token }}
PARENT_SHA: ${{ needs.metadata.outputs.parent_sha }}
GITLINK_SHA: ${{ needs.metadata.outputs.gitlink_sha }}
BUILD_VERSION: ${{ needs.metadata.outputs.build_version }}
CORRELATION_ID: ${{ needs.metadata.outputs.correlation_id }}
run: |
set -euo pipefail
expected_title="marketing-build correlation=$CORRELATION_ID parent=$PARENT_SHA gitlink=$GITLINK_SHA version=$BUILD_VERSION"
deadline=$((SECONDS + 3600))
run_id=""
while (( SECONDS < deadline )); do
runs="$(gh api "repos/fluxerapp/marketing/actions/workflows/build-marketing.yaml/runs?event=workflow_dispatch&per_page=100" --jq '[.workflow_runs[] | {id, event, display_title, status, conclusion}]')"
matches="$(jq --arg title "$expected_title" '[.[] | select(.event == "workflow_dispatch" and .display_title == $title)]' <<<"$runs")"
count="$(jq 'length' <<<"$matches")"
if [[ "$count" == "1" ]]; then
run_id="$(jq -r '.[0].id' <<<"$matches")"
break
fi
if [[ "$count" != "0" ]]; then
echo "::error::Private build correlation matched multiple workflow runs."
exit 1
fi
sleep 10
done
if [[ -z "$run_id" ]]; then
echo "::error::Timed out waiting for the private build dispatch to appear."
exit 1
fi
while (( SECONDS < deadline )); do
runs="$(gh api "repos/fluxerapp/marketing/actions/workflows/build-marketing.yaml/runs?event=workflow_dispatch&per_page=100" --jq '[.workflow_runs[] | {id, event, display_title, status, conclusion}]')"
matches="$(jq --arg title "$expected_title" '[.[] | select(.event == "workflow_dispatch" and .display_title == $title)]' <<<"$runs")"
if [[ "$(jq 'length' <<<"$matches")" != "1" || "$(jq -r '.[0].id' <<<"$matches")" != "$run_id" ]]; then
echo "::error::Private build correlation is missing or ambiguous."
exit 1
fi
run="$(jq '.[0]' <<<"$matches")"
status="$(jq -r '.status' <<<"$run")"
conclusion="$(jq -r '.conclusion // empty' <<<"$run")"
if [[ "$status" == "completed" ]]; then
if [[ "$conclusion" != "success" ]]; then
echo "::error::Private marketing build concluded with $conclusion."
exit 1
fi
echo "Private marketing build completed successfully."
exit 0
fi
sleep 15
done
echo "::error::Timed out waiting for the private marketing build."
exit 1
-51
View File
@@ -1,51 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
name: finalise release
on:
workflow_dispatch:
inputs:
build-version:
description: "Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes)"
type: string
required: true
fragment-run-id:
description: "Workflow run id that produced the release-fragment-* artifacts"
type: string
required: true
permissions:
actions: read
contents: write
defaults:
run:
shell: bash
jobs:
finalise:
name: finalise GitHub release manifest
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
with:
toolchain: "1.93.0"
- name: Download GitHub release fragments
env:
GH_TOKEN: ${{ github.token }}
run: >-
gh run download "${{ inputs.fragment-run-id }}"
--pattern "release-fragment-*"
--dir release-out/fragments
- name: Finalise release
env:
GH_TOKEN: ${{ github.token }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- release
finalise
--build-version "${{ inputs.build-version }}"
+2 -2
View File
@@ -71,7 +71,7 @@ jobs:
GH_TOKEN: ${{ steps.create-token.outputs.token }}
run: |
set -euo pipefail
if [[ -z "$(git status --porcelain -- fluxer_app/src/features/i18n/locales fluxer_marketing/locales packages/errors/src/i18n fluxer_api/pkgs/email/src/email_i18n fluxer_api/src/api/content_i18n)" ]]; then
if [[ -z "$(git status --porcelain -- fluxer_app/src/features/i18n/locales packages/errors/src/i18n fluxer_api/pkgs/email/src/email_i18n fluxer_api/src/api/content_i18n)" ]]; then
echo "No source catalog changes."
exit 0
fi
@@ -79,7 +79,7 @@ jobs:
git config user.name "fluxer-ci[bot]"
git config user.email "${{ vars.FLUXER_CI_APP_USER_ID }}+fluxer-ci[bot]@users.noreply.github.com"
git switch -c "$SOURCE_BRANCH"
git add fluxer_app/src/features/i18n/locales fluxer_marketing/locales packages/errors/src/i18n fluxer_api/pkgs/email/src/email_i18n fluxer_api/src/api/content_i18n
git add fluxer_app/src/features/i18n/locales packages/errors/src/i18n fluxer_api/pkgs/email/src/email_i18n fluxer_api/src/api/content_i18n
git commit -m "chore(i18n): refresh source catalogs"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git fetch origin "$SOURCE_BRANCH" || true
+2 -2
View File
@@ -77,14 +77,14 @@ jobs:
GH_TOKEN: ${{ steps.create-token.outputs.token }}
run: |
set -euo pipefail
if [[ -z "$(git status --porcelain -- fluxer_app/src/features/i18n/locales packages/errors/src/i18n fluxer_api/pkgs/email/src/email_i18n fluxer_api/src/api/content_i18n)" ]]; then
if [[ -z "$(git status --porcelain -- fluxer_app/src/features/i18n/locales packages/i18n/marketing packages/errors/src/i18n fluxer_api/pkgs/email/src/email_i18n fluxer_api/src/api/content_i18n)" ]]; then
echo "No generated catalog changes."
exit 0
fi
git config user.name "fluxer-ci[bot]"
git config user.email "${{ vars.FLUXER_CI_APP_USER_ID }}+fluxer-ci[bot]@users.noreply.github.com"
git add fluxer_app/src/features/i18n/locales packages/errors/src/i18n fluxer_api/pkgs/email/src/email_i18n fluxer_api/src/api/content_i18n
git add fluxer_app/src/features/i18n/locales packages/i18n/marketing packages/errors/src/i18n fluxer_api/pkgs/email/src/email_i18n fluxer_api/src/api/content_i18n
git commit -m "i18n: compile Weblate catalogs"
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
git push origin "HEAD:$WEBLATE_BRANCH"
-282
View File
@@ -1,282 +0,0 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
name: release all builds
on:
workflow_dispatch:
inputs:
build-version:
description: "Explicit Fluxer CalVer build version (YYYY.MDD.MICRO, UTC HHMMSS without leading zeroes) to use instead of automatic UTC clock allocation"
type: string
required: false
default: ""
permissions:
actions: read
contents: write
packages: write
defaults:
run:
shell: bash
concurrency:
group: release-all-${{ inputs['build-version'] || github.run_id }}
cancel-in-progress: false
jobs:
approve:
name: approve release build
runs-on: ubuntu-24.04
environment: builds
timeout-minutes: 5
steps:
- name: approved
run: echo "Release build approved."
meta:
name: resolve metadata
needs: approve
if: ${{ !failure() && !cancelled() }}
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
build_version: ${{ steps.vars.outputs.build_version }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
with:
toolchain: "1.93.0"
- name: set variables
id: vars
env:
GH_TOKEN: ${{ github.token }}
FLUXER_BUILD_VERSION: ${{ inputs['build-version'] }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- resolve-calver
--github-output
build_admin:
needs: meta
uses: ./.github/workflows/build-admin.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
build_api:
needs: meta
uses: ./.github/workflows/build-api.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
build_app_proxy:
needs: meta
uses: ./.github/workflows/build-app-proxy.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
build_app_proxy_self_hosted:
needs: meta
uses: ./.github/workflows/build-app-proxy-self-hosted.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
build_docs:
needs: meta
uses: ./.github/workflows/build-docs.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
build_gateway:
needs: meta
uses: ./.github/workflows/build-gateway.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
build_gifs:
needs: meta
uses: ./.github/workflows/build-gifs.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
build_marketing:
needs: meta
uses: ./.github/workflows/build-marketing.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
build_media_proxy:
needs: meta
uses: ./.github/workflows/build-media-proxy.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
build_messages:
needs: meta
uses: ./.github/workflows/build-messages.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
build_snowflakes:
needs: meta
uses: ./.github/workflows/build-snowflakes.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
build_static:
needs: meta
uses: ./.github/workflows/build-static.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
build_unfurl:
needs: meta
uses: ./.github/workflows/build-unfurl.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
build_users:
needs: meta
uses: ./.github/workflows/build-users.yaml
with:
build-version: ${{ needs.meta.outputs.build_version }}
finalise-release: false
approval-required: false
secrets: inherit
release_assets:
name: package Helm/self-hosting
if: ${{ !failure() && !cancelled() }}
needs:
- meta
- build_admin
- build_api
- build_app_proxy
- build_app_proxy_self_hosted
- build_docs
- build_gateway
- build_gifs
- build_marketing
- build_media_proxy
- build_messages
- build_snowflakes
- build_static
- build_unfurl
- build_users
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
with:
toolchain: "1.93.0"
- name: Set up Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310
- name: Publish self-hosting bundle
env:
GH_TOKEN: ${{ github.token }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- release
publish-self-hosting
--build-version "${{ needs.meta.outputs.build_version }}"
- name: Publish Helm chart bundle
env:
GH_TOKEN: ${{ github.token }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- release
publish-helm
--build-version "${{ needs.meta.outputs.build_version }}"
- name: Upload release asset fragments
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
with:
name: release-fragment-release-assets
path: release-out/fragments/*.json
if-no-files-found: error
retention-days: 14
finalise:
name: finalise GitHub release manifest
if: ${{ !failure() && !cancelled() }}
needs:
- meta
- build_admin
- build_api
- build_app_proxy
- build_app_proxy_self_hosted
- build_docs
- build_gateway
- build_gifs
- build_marketing
- build_media_proxy
- build_messages
- build_snowflakes
- build_static
- build_unfurl
- build_users
- release_assets
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9
with:
toolchain: "1.93.0"
- name: Download GitHub release fragments
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
with:
pattern: release-fragment-*
path: release-out/fragments
merge-multiple: true
- name: Finalise GitHub release manifest
env:
GH_TOKEN: ${{ github.token }}
run: >-
cargo run --locked --quiet --manifest-path tools/ci/Cargo.toml -- release
finalise
--build-version "${{ needs.meta.outputs.build_version }}"
+1 -1
View File
@@ -125,7 +125,7 @@ jobs:
libwebp-dev
- name: Install Node.js dependencies
run: pnpm --filter fluxer_admin --filter fluxer_marketing install
run: pnpm --filter fluxer_admin install
- name: Check formatting
run: cargo fmt --all -- --check
-2
View File
@@ -41,8 +41,6 @@
/app-dist-output/
/artifacts/
/release-input/
/release-out/
/s3_payload/
/upload_staging/
+4
View File
@@ -0,0 +1,4 @@
[submodule "fluxer_marketing"]
path = fluxer_marketing
url = https://github.com/fluxerapp/marketing.git
update = none
Generated
-443
View File
@@ -2,12 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "accept-language"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f27d075294830fcab6f66e320dab524bc6d048f4a151698e153205559113772"
[[package]]
name = "adler2"
version = "2.0.1"
@@ -896,25 +890,6 @@ dependencies = [
"either",
]
[[package]]
name = "calendrical_calculations"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5abbd6eeda6885048d357edc66748eea6e0268e3dd11f326fff5bd248d779c26"
dependencies = [
"core_maths",
"displaydoc",
]
[[package]]
name = "caseless"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8"
dependencies = [
"unicode-normalization",
]
[[package]]
name = "cast"
version = "0.3.0"
@@ -1103,29 +1078,6 @@ version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789"
[[package]]
name = "comrak"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aac0b255932a9cd52fbfd664b67957f9f2e095ae4711cb0e41b4e291edef94c2"
dependencies = [
"caseless",
"entities",
"finl_unicode",
"jetscii",
"phf 0.13.1",
"phf_codegen 0.13.1",
"rustc-hash",
"smallvec",
"typed-arena",
]
[[package]]
name = "concat-string"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7439becb5fafc780b6f4de382b1a7a3e70234afe783854a4702ee8adbb838609"
[[package]]
name = "concurrent-queue"
version = "2.5.0"
@@ -1173,15 +1125,6 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core_maths"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
dependencies = [
"libm",
]
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -1681,79 +1624,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "email_address"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
dependencies = [
"serde",
]
[[package]]
name = "encoding"
version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec"
dependencies = [
"encoding-index-japanese",
"encoding-index-korean",
"encoding-index-simpchinese",
"encoding-index-singlebyte",
"encoding-index-tradchinese",
]
[[package]]
name = "encoding-index-japanese"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding-index-korean"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding-index-simpchinese"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding-index-singlebyte"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding-index-tradchinese"
version = "1.20141219.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18"
dependencies = [
"encoding_index_tests",
]
[[package]]
name = "encoding_index_tests"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569"
[[package]]
name = "entities"
version = "1.0.1"
@@ -1834,39 +1704,12 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "filetime"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
dependencies = [
"cfg-if",
"libc",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "finl_unicode"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5"
[[package]]
name = "fixed_decimal"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79c3c892f121fff406e5dd6b28c1b30096b95111c30701a899d4f2b18da6d1bd"
dependencies = [
"displaydoc",
"smallvec",
"writeable",
]
[[package]]
name = "flagset"
version = "0.4.7"
@@ -1895,14 +1738,12 @@ dependencies = [
"bytes",
"chrono",
"clap",
"flate2",
"hex",
"md-5",
"reqwest",
"serde",
"serde_json",
"sha2 0.11.0",
"tar",
"tempfile",
"tokio",
"walkdir",
@@ -1975,17 +1816,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "fluxer-marketing-update-gettext-catalogs"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"serde_json",
"syn",
"tempfile",
]
[[package]]
name = "fluxer-media-proxy"
version = "0.1.0"
@@ -2217,42 +2047,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "fluxer_marketing"
version = "0.1.0"
dependencies = [
"accept-language",
"ammonia",
"anyhow",
"axum",
"base64",
"comrak",
"cookie",
"email_address",
"fluxer_common",
"gettext",
"hmac 0.13.0",
"http-body-util",
"icu_datetime",
"icu_locale",
"maud",
"mime_guess",
"moka",
"polib",
"reqwest",
"serde",
"serde_json",
"sha2 0.11.0",
"syn",
"time",
"tokio",
"tower",
"tower-http",
"tracing",
"tracing-subscriber",
"urlencoding",
]
[[package]]
name = "fnv"
version = "1.0.7"
@@ -2445,16 +2239,6 @@ dependencies = [
"wasip3",
]
[[package]]
name = "gettext"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ebb594e753d5997e4be036e5a8cf048ab9414352870fb45c779557bbc9ba971"
dependencies = [
"byteorder",
"encoding",
]
[[package]]
name = "gif"
version = "0.14.2"
@@ -2816,29 +2600,6 @@ dependencies = [
"cc",
]
[[package]]
name = "icu_calendar"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2b2acc6263f494f1df50685b53ff8e57869e47d5c6fe39c23d518ae9a4f3e45"
dependencies = [
"calendrical_calculations",
"displaydoc",
"icu_calendar_data",
"icu_locale",
"icu_locale_core",
"icu_provider",
"ixdtf",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_calendar_data"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "118577bcf3a0fa7c6ac0a7d6e951814da84ee56b9b1f68fb4d8d10b08cefaf4d"
[[package]]
name = "icu_collections"
version = "2.2.0"
@@ -2853,73 +2614,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "icu_datetime"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989d56ea5bbc43ae2b4e0388874b002884eaf4ed3a76c84a6c8c5ad575e04d72"
dependencies = [
"displaydoc",
"fixed_decimal",
"icu_calendar",
"icu_datetime_data",
"icu_decimal",
"icu_locale",
"icu_locale_core",
"icu_pattern",
"icu_plurals",
"icu_provider",
"icu_time",
"potential_utf",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_datetime_data"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40d3cc1b690d9703202bc319692ac8a1f3a6390686f0930ff40542450fa34f0b"
[[package]]
name = "icu_decimal"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "288247df2e32aa776ac54fdd64de552149ac43cb840f2761811f0e8d09719dd4"
dependencies = [
"displaydoc",
"fixed_decimal",
"icu_decimal_data",
"icu_locale",
"icu_locale_core",
"icu_plurals",
"icu_provider",
"writeable",
"zerovec",
]
[[package]]
name = "icu_decimal_data"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f14a5ca9e8af29eef62064f269078424283d90dbaffeac5225addf62aaabc22"
[[package]]
name = "icu_locale"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5a396343c7208121dc86e35623d3dfe19814a7613cfd14964994cdc9c9a2e26"
dependencies = [
"icu_collections",
"icu_locale_core",
"icu_locale_data",
"icu_provider",
"potential_utf",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.2.0"
@@ -2928,18 +2622,11 @@ checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
dependencies = [
"displaydoc",
"litemap",
"serde",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locale_data"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fdcc9ac77c6d74ff5cf6e65ef3181d6af32003b16fce3a77fb451d2f695993"
[[package]]
name = "icu_normalizer"
version = "2.2.0"
@@ -2960,38 +2647,6 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
[[package]]
name = "icu_pattern"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c4c568054ffe735398a9f4c55aec37ad7c768844553cc0978f09cc9b933a1fb"
dependencies = [
"displaydoc",
"either",
"serde",
"writeable",
"zerovec",
]
[[package]]
name = "icu_plurals"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a50023f1d49ad5c4333380328a0d4a19e4b9d6d842ec06639affd5ba47c8103"
dependencies = [
"fixed_decimal",
"icu_locale",
"icu_plurals_data",
"icu_provider",
"zerovec",
]
[[package]]
name = "icu_plurals_data"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8485497155dc865f901decb93ecc20d3e467df67bfeceb91e3ba34e2b11e8e1d"
[[package]]
name = "icu_properties"
version = "2.2.0"
@@ -3020,8 +2675,6 @@ checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
dependencies = [
"displaydoc",
"icu_locale_core",
"serde",
"stable_deref_trait",
"writeable",
"yoke",
"zerofrom",
@@ -3029,30 +2682,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "icu_time"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3af0c141da0a61d4f6970cd1d5f4b388b17ea22f8124f8f6049d3d5147586a"
dependencies = [
"calendrical_calculations",
"displaydoc",
"icu_calendar",
"icu_locale_core",
"icu_provider",
"icu_time_data",
"ixdtf",
"serde",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_time_data"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f2f8aeca682d874a5247084aa4fb7d1cef9ba45d889c21209a8818dcaaa0ec9"
[[package]]
name = "id-arena"
version = "2.3.0"
@@ -3177,18 +2806,6 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "ixdtf"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ceaf4c6c48465bead8cb6a0b7c4ee0c86ecbb31239032b9c66ab9a08d2f3ee1"
[[package]]
name = "jetscii"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47f142fe24a9c9944451e8349de0a56af5f3e7226dc46f3ed4d4ecc0b85af75e"
[[package]]
name = "jni"
version = "0.22.4"
@@ -3278,12 +2895,6 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libm"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
version = "0.1.17"
@@ -3293,15 +2904,6 @@ dependencies = [
"libc",
]
[[package]]
name = "linereader"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d921fea6860357575519aca014c6e22470585accdd543b370c404a8a72d0dd1d"
dependencies = [
"memchr",
]
[[package]]
name = "linkify"
version = "0.11.0"
@@ -3934,16 +3536,6 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "polib"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee83e5a284d919e51b071969bbf2d12d6943857aab02d84c5cc449373c9f3b7b"
dependencies = [
"concat-string",
"linereader",
]
[[package]]
name = "portable-atomic"
version = "1.13.1"
@@ -3987,8 +3579,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
dependencies = [
"serde_core",
"writeable",
"zerovec",
]
@@ -5284,17 +4874,6 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "tar"
version = "0.4.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
dependencies = [
"filetime",
"libc",
"xattr",
]
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -5396,7 +4975,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
dependencies = [
"displaydoc",
"serde_core",
"zerovec",
]
@@ -5728,12 +5306,6 @@ version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
[[package]]
name = "typed-arena"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typed-path"
version = "0.12.3"
@@ -6471,9 +6043,6 @@ name = "writeable"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
dependencies = [
"either",
]
[[package]]
name = "wyhash"
@@ -6496,16 +6065,6 @@ dependencies = [
"tls_codec",
]
[[package]]
name = "xattr"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
dependencies = [
"libc",
"rustix",
]
[[package]]
name = "xmlparser"
version = "0.13.6"
@@ -6605,7 +6164,6 @@ dependencies = [
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
@@ -6614,7 +6172,6 @@ version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
dependencies = [
"serde",
"yoke",
"zerofrom",
"zerovec-derive",
+1 -2
View File
@@ -3,7 +3,6 @@ members = [
"fluxer_admin",
"fluxer_app_proxy",
"fluxer_common",
"fluxer_marketing",
"fluxer_media_proxy",
"fluxer_gifs",
"fluxer_svc",
@@ -13,12 +12,12 @@ members = [
"tools/content/update-frozen-snapshot",
"tools/dev",
"tools/i18n_auto",
"tools/marketing/update-gettext-catalogs",
"fluxer_users",
"fluxer_unfurl",
"packages/markdown_parser/rust",
]
exclude = [
"fluxer_marketing",
"packages/markdown_parser/rust/fuzz",
"fluxer_desktop/native/webrtc-sender/vendor/tract-linalg-0.19.16",
"fluxer_desktop/native/webrtc-sender/vendor/tract-linalg-0.23.1",
-1
View File
@@ -145,7 +145,6 @@
"!fluxer_static",
"!packages/fonts",
"!fluxer_admin/static/htmx.min.js",
"!fluxer_marketing/static/htmx.min.js",
"!fluxer_api/src/api/openapi/openapi.json"
],
"ignoreUnknown": true
+1 -1
View File
@@ -17,7 +17,7 @@ FLUXER_GATEWAY_ENDPOINT=ws://localhost:8088/gateway
FLUXER_MEDIA_ENDPOINT=http://localhost:8088/media
FLUXER_STATIC_CDN_ENDPOINT=http://localhost:8088
FLUXER_ADMIN_ENDPOINT=http://localhost:8088/admin
FLUXER_MARKETING_ENDPOINT=http://localhost:8088/marketing
FLUXER_MARKETING_ENDPOINT=https://fluxer.app
FLUXER_TRUST_CLIENT_IP_HEADER=true
FLUXER_CLIENT_IP_HEADER_NAME=x-forwarded-for
+3
View File
@@ -276,6 +276,9 @@ export function buildAPIConfigFromMaster(master: MasterConfig): APIConfig {
ipinfoApiKey: master.integrations.risk_integration.ipinfo_api_key || undefined,
accountPolicyDsl: master.integrations.risk_integration.account_policy_dsl,
},
blocklistFeeds: {
enabled: master.integrations.blocklist_feeds.enabled ?? !master.instance.self_hosted,
},
captcha: {
enabled: master.integrations.captcha.enabled,
provider: master.integrations.captcha.provider,
+2 -30
View File
@@ -2,7 +2,7 @@
import {getSameIpDecisionKey} from '@fluxer/ip_utils/src/IpAddress';
import {createUserID} from '../BrandedTypes';
import {deleteOneOrMany, fetchMany, fetchOne, fetchPage, upsertOne} from '../database/CassandraQueryExecution';
import {deleteOneOrMany, fetchMany, fetchOne, upsertOne} from '../database/CassandraQueryExecution';
import type {
AdminAuditLogRow,
BannedAvatarHashRow,
@@ -30,13 +30,7 @@ import {
} from '../Tables';
import {parseIpBanEntry, tryParseSingleIp} from '../utils/IpRangeUtils';
import {canonicalizeStoredPhrase} from '../utils/PhraseBlocklistNormalization';
import type {
AdminAuditLog,
BannedIpEntry,
BannedIpKind,
DisposableEmailDomainPage,
IAdminRepository,
} from './IAdminRepository';
import type {AdminAuditLog, BannedIpEntry, BannedIpKind, IAdminRepository} from './IAdminRepository';
const FETCH_AUDIT_LOG_BY_ID_QUERY = AdminAuditLogs.select({
where: AdminAuditLogs.where.eq('log_id'),
@@ -51,8 +45,6 @@ const IS_EMAIL_BANNED_QUERY = BannedEmails.select({
const IS_EMAIL_DOMAIN_SUSPICIOUS_QUERY = SuspiciousEmailDomains.select({
where: SuspiciousEmailDomains.where.eq('domain'),
});
const createLoadSuspiciousEmailDomainsQuery = (limit?: number) =>
limit ? SuspiciousEmailDomains.select({limit}) : SuspiciousEmailDomains.select();
const IS_EMAIL_DOMAIN_DISPOSABLE_QUERY = DisposableEmailDomains.select({
where: DisposableEmailDomains.where.eq('domain'),
});
@@ -273,13 +265,6 @@ export class AdminRepository implements IAdminRepository {
await deleteOneOrMany(SuspiciousEmailDomains.deleteByPk({domain: domainLower}));
}
async listSuspiciousEmailDomains(limit?: number): Promise<Array<string>> {
const rows = await fetchMany<{
domain: string;
}>(createLoadSuspiciousEmailDomainsQuery(limit).bind({}));
return rows.map((row) => row.domain);
}
async isEmailDomainDisposable(domain: string): Promise<boolean> {
const domainLower = domain.toLowerCase();
if (isAccountPolicyContactDomainReputationExempt(domainLower)) return false;
@@ -306,19 +291,6 @@ export class AdminRepository implements IAdminRepository {
return rows.map((row) => row.domain);
}
async listDisposableEmailDomainsPage(limit: number, pageState?: string | null): Promise<DisposableEmailDomainPage> {
const page = await fetchPage<{
domain: string;
}>(createLoadDisposableEmailDomainsQuery().bind({}), undefined, {
pageSize: limit,
pageState,
});
return {
domains: page.rows.map((row) => row.domain),
pageState: page.pageState,
};
}
async isPhraseBanned(phrase: string): Promise<boolean> {
const phraseLower = canonicalizeStoredPhrase(phrase);
const result = await fetchOne<{
@@ -32,11 +32,6 @@ export interface BannedIpEntry {
createdAt: Date | null;
}
export interface DisposableEmailDomainPage {
domains: Array<string>;
pageState: string | null;
}
export abstract class IAdminRepository {
abstract createAuditLog(log: AdminAuditLogRow): Promise<AdminAuditLog>;
@@ -68,8 +63,6 @@ export abstract class IAdminRepository {
abstract removeSuspiciousEmailDomain(domain: string): Promise<void>;
abstract listSuspiciousEmailDomains(limit?: number): Promise<Array<string>>;
abstract isEmailDomainDisposable(domain: string): Promise<boolean>;
abstract addDisposableEmailDomain(domain: string): Promise<void>;
@@ -78,8 +71,6 @@ export abstract class IAdminRepository {
abstract listDisposableEmailDomains(limit?: number): Promise<Array<string>>;
abstract listDisposableEmailDomainsPage(limit: number, pageState?: string | null): Promise<DisposableEmailDomainPage>;
abstract isPhraseBanned(phrase: string): Promise<boolean>;
abstract banPhrase(phrase: string): Promise<void>;
+1 -64
View File
@@ -13,11 +13,7 @@ import type {ILogger} from '../ILogger';
import {JobLedgerRepository} from '../jobs/JobLedgerRepository';
import {startAbuseReplicationSubscriber, stopAbuseReplicationSubscriber} from '../middleware/AbusiveIpAutoBanner';
import {ipBanCache} from '../middleware/IpBanMiddleware';
import {
getRiskCacheManagerInstance,
initializeServiceSingletons,
shutdownReportService,
} from '../middleware/ServiceMiddleware';
import {initializeServiceSingletons, shutdownReportService} from '../middleware/ServiceMiddleware';
import {
ensureVoiceResourcesInitialized,
getKVClient,
@@ -40,57 +36,6 @@ import {JetStreamWorkerQueue} from '../worker/JetStreamWorkerQueue';
import {WorkerService} from '../worker/WorkerService';
let jsConnectionManager: JetStreamConnectionManager | null = null;
let riskCacheRefreshInterval: NodeJS.Timeout | null = null;
let riskCacheRefreshInFlight = false;
const RISK_CACHE_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
async function refreshRiskCache(logger: ILogger, source: 'startup' | 'interval'): Promise<void> {
if (riskCacheRefreshInFlight) {
return;
}
riskCacheRefreshInFlight = true;
try {
const result = await getRiskCacheManagerInstance().refresh();
if (result.subtaskErrors.length > 0) {
logger.warn({source, errors: result.subtaskErrors}, 'Risk cache refresh completed with errors');
return;
}
logger.info(
{
source,
disposableDomainCount: result.disposableDomainCount,
},
source === 'startup' ? 'Risk cache initialized on API startup' : 'Risk cache refresh complete on API',
);
} catch (error) {
if (source === 'startup') {
logger.warn({error}, 'Risk cache initialisation failed on API startup');
return;
}
logger.warn({error}, 'Periodic risk cache refresh failed on API');
} finally {
riskCacheRefreshInFlight = false;
}
}
function startRiskCacheRefreshLoop(logger: ILogger): void {
if (riskCacheRefreshInterval) {
return;
}
riskCacheRefreshInterval = setInterval(() => {
void refreshRiskCache(logger, 'interval');
}, RISK_CACHE_REFRESH_INTERVAL_MS);
}
function stopRiskCacheRefreshLoop(): void {
if (!riskCacheRefreshInterval) {
return;
}
clearInterval(riskCacheRefreshInterval);
riskCacheRefreshInterval = null;
}
export function createInitializer(config: APIConfig, logger: ILogger): () => Promise<void> {
return async (): Promise<void> => {
try {
@@ -171,8 +116,6 @@ export function createInitializer(config: APIConfig, logger: ILogger): () => Pro
logger.info('Profile substring blocklist cache initialized');
await initializeServiceSingletons();
logger.info('Service singletons initialized');
await refreshRiskCache(logger, 'startup');
startRiskCacheRefreshLoop(logger);
if (!config.dev.testModeEnabled) {
jsConnectionManager = new JetStreamConnectionManager({
url: config.nats.jetStreamUrl,
@@ -285,12 +228,6 @@ export function createShutdown(logger: ILogger): () => Promise<void> {
} catch (error) {
logger.error({error}, 'Error shutting down search service');
}
try {
stopRiskCacheRefreshLoop();
logger.info('Risk cache refresh loop shut down');
} catch (error) {
logger.error({error}, 'Error shutting down risk cache refresh loop');
}
try {
ipBanCache.shutdown();
logger.info('IP ban cache shut down');
+3
View File
@@ -178,6 +178,9 @@ export interface APIConfig {
ipinfoApiKey?: string;
accountPolicyDsl?: unknown;
};
blocklistFeeds: {
enabled: boolean;
};
captcha: {
enabled: boolean;
provider: 'hcaptcha' | 'turnstile' | 'none';
@@ -561,14 +561,14 @@ WHERE NOT $6`,
private async patch(meta: KvQueryMeta, params: CassandraParams, db: PostgresQueryable): Promise<void> {
const key = rowKeyFromParams(meta, params);
const existing = await this.getRow(meta, key, db);
const base = existing ?? paramsRow(params, (meta.pkColumns ?? meta.table.primaryKey) as ReadonlyArray<string>);
const stored = await this.getStoredRow(meta, key, db);
const base = stored?.row ?? paramsRow(params, (meta.pkColumns ?? meta.table.primaryKey) as ReadonlyArray<string>);
const next = {...base};
for (const column of meta.patchKeys ?? []) {
next[column] = column in params ? params[column] : null;
}
const ttl = ttlExpiresAt(meta, params);
const expiresAt = ttl === undefined ? await this.getExpiresAt(meta, key, db) : ttl;
const expiresAt = ttl === undefined ? (stored?.expiresAt ?? null) : ttl;
await db.query(
`INSERT INTO ${this.table} (table_name, partition_key, row_key, row_data, expires_at, updated_at)
VALUES ($1, $2, $3, $4::jsonb, $5, now())
@@ -592,20 +592,20 @@ DO UPDATE SET partition_key = EXCLUDED.partition_key, row_data = EXCLUDED.row_da
]);
}
private async getRow(meta: KvQueryMeta, key: string, db: PostgresQueryable): Promise<Row | null> {
const result = await db.query<StoredRow>(
`SELECT row_key, row_data FROM ${this.table} WHERE table_name = $1 AND row_key = $2 AND (expires_at IS NULL OR expires_at > now()) LIMIT 1`,
private async getStoredRow(
meta: KvQueryMeta,
key: string,
db: PostgresQueryable,
): Promise<{row: Row; expiresAt: Date | null} | null> {
const result = await db.query<StoredRow & {expires_at: Date | null}>(
`SELECT row_key, row_data, expires_at FROM ${this.table} WHERE table_name = $1 AND row_key = $2 AND (expires_at IS NULL OR expires_at > now()) LIMIT 1`,
[meta.table.name, key],
);
const row = result.rows[0];
return row ? decodeRow(row.row_data) : null;
return row ? {row: decodeRow(row.row_data), expiresAt: row.expires_at ?? null} : null;
}
private async getExpiresAt(meta: KvQueryMeta, key: string, db: PostgresQueryable): Promise<Date | null> {
const result = await db.query<{expires_at: Date | null}>(
`SELECT expires_at FROM ${this.table} WHERE table_name = $1 AND row_key = $2 AND (expires_at IS NULL OR expires_at > now()) LIMIT 1`,
[meta.table.name, key],
);
return result.rows[0]?.expires_at ?? null;
private async getRow(meta: KvQueryMeta, key: string, db: PostgresQueryable): Promise<Row | null> {
return (await this.getStoredRow(meta, key, db))?.row ?? null;
}
}
@@ -82,8 +82,8 @@ type FormatMapping = {
const FORMAT_MAPPINGS: Record<DesktopFormat, Partial<Record<DesktopPlatform, FormatMapping>>> = {
setup: {win32: {ext: '.exe', arch: {x64: 'x64', arm64: 'arm64'}}},
dmg: {darwin: {ext: '.dmg', arch: {x64: 'x64', arm64: 'arm64'}}},
zip: {darwin: {ext: '.zip', arch: {x64: 'x64', arm64: 'arm64'}}},
dmg: {darwin: {ext: '.dmg', arch: {x64: ['universal', 'x64'], arm64: ['universal', 'arm64']}}},
zip: {darwin: {ext: '.zip', arch: {x64: ['universal', 'x64'], arm64: ['universal', 'arm64']}}},
appimage: {linux: {ext: '.AppImage', arch: {x64: 'x86_64', arm64: ['aarch64', 'arm64']}}},
deb: {linux: {ext: '.deb', arch: {x64: 'amd64', arm64: 'arm64'}}},
rpm: {linux: {ext: '.rpm', arch: {x64: 'x86_64', arm64: 'aarch64'}}},
@@ -60,8 +60,6 @@ import {CassandraHistoricalOutcomeRepository} from '../risk/HistoricalOutcomeRep
import {buildIpInfoCache, buildIpInfoRequestAuditLogger} from '../risk/IpInfoCacheFactory';
import {CassandraRegistrationEventsRepository} from '../risk/RegistrationEventsRepository';
import {CassandraRiskAssessmentRepository} from '../risk/RiskAssessmentRepository';
import {buildRiskCacheLoaders} from '../risk/RiskCacheLoaders';
import {RiskCacheManager} from '../risk/RiskCacheManager';
import {createRiskToolbox} from '../risk/RiskToolboxFactory';
import {CassandraSuspiciousIpRepository} from '../risk/SuspiciousIpRepository';
import {RpcService} from '../rpc/RpcService';
@@ -192,24 +190,6 @@ export function shutdownReportService(): void {
}
}
let _riskCacheManager: RiskCacheManager | null = null;
function getRiskCacheManager(): RiskCacheManager {
if (!_riskCacheManager) {
_riskCacheManager = new RiskCacheManager({
logger: Logger,
...buildRiskCacheLoaders({
adminRepository: getAdminRepository(),
}),
});
}
return _riskCacheManager;
}
export function getRiskCacheManagerInstance(): RiskCacheManager {
return getRiskCacheManager();
}
let _inboundSmsChallengeService: InboundSmsChallengeService | null = null;
function getInboundSmsChallengeService(): InboundSmsChallengeService {
@@ -311,13 +291,12 @@ function getRegistrationRiskEvaluator(): IRegistrationRiskEvaluator {
_registrationRiskEvaluator = noopRegistrationRiskEvaluator;
return _registrationRiskEvaluator;
}
const cacheManager = getRiskCacheManager();
const ipInfoService = getIpInfoService();
const ipInfoChecker = Config.risk.ipinfoApiKey ? createIpInfoChecker({ipInfoService}) : undefined;
const cacheService = getCacheService();
const reverseDnsLookup = createReverseDnsLookup({cacheService});
const toolbox = createRiskToolbox({
disposableDomainsRef: cacheManager.disposableDomainsRef,
adminRepository: getAdminRepository(),
ipInfoChecker,
reverseDnsLookup,
ipInfoService,
@@ -1,23 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {IAdminRepository} from '../admin/IAdminRepository';
import type {RiskCacheLoaders} from './RiskCacheManager';
interface BuildRiskCacheLoadersDeps {
adminRepository: Pick<IAdminRepository, 'listSuspiciousEmailDomains' | 'listDisposableEmailDomains'>;
}
export function buildRiskCacheLoaders(deps: BuildRiskCacheLoadersDeps): RiskCacheLoaders {
return {
loadDisposableDomains: async () => {
const [suspicious, disposable] = await Promise.all([
deps.adminRepository.listSuspiciousEmailDomains(),
deps.adminRepository.listDisposableEmailDomains(),
]);
const set = new Set<string>();
for (const d of suspicious) set.add(d.toLowerCase());
for (const d of disposable) set.add(d.toLowerCase());
return set;
},
};
}
@@ -1,64 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
interface MutableRef<T> {
current: T;
}
export type ReadonlyRiskCacheRef<T> = {
readonly current: T;
};
interface RiskCacheManagerLogger {
info(payload: object, msg: string): void;
warn(payload: object, msg: string): void;
}
export interface RiskCacheLoaders {
loadDisposableDomains: () => Promise<ReadonlySet<string>>;
}
interface RiskCacheManagerOptions extends RiskCacheLoaders {
logger?: RiskCacheManagerLogger;
}
interface RiskCacheRefreshResult {
disposableDomainCount: number;
subtaskErrors: ReadonlyArray<{
step: string;
error: string;
}>;
}
export class RiskCacheManager {
private readonly _disposable: MutableRef<ReadonlySet<string>> = {current: new Set()};
readonly disposableDomainsRef: ReadonlyRiskCacheRef<ReadonlySet<string>> = this._disposable;
private readonly logger: RiskCacheManagerLogger | undefined;
private readonly loaders: RiskCacheLoaders;
constructor(opts: RiskCacheManagerOptions) {
this.logger = opts.logger;
this.loaders = {
loadDisposableDomains: opts.loadDisposableDomains,
};
}
async refresh(): Promise<RiskCacheRefreshResult> {
const subtaskErrors: Array<{
step: string;
error: string;
}> = [];
try {
const set = await this.loaders.loadDisposableDomains();
this._disposable.current = set;
this.logger?.info({count: set.size}, 'RiskCacheManager: loaded disposable domains from DB');
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
this.logger?.warn({step: 'disposable_domains', err: message}, 'RiskCacheManager: subtask failed');
subtaskErrors.push({step: 'disposable_domains', error: message});
}
return {
disposableDomainCount: this._disposable.current.size,
subtaskErrors,
};
}
}
@@ -2,6 +2,7 @@
import type {ICacheService} from '@pkgs/cache/src/ICacheService';
import type {IpInfoService} from '@pkgs/geoip/src/IpInfoService';
import type {IAdminRepository} from '../admin/IAdminRepository';
import {createDisposableDomainChecker} from './adapters/DisposableDomainChecker';
import {createDnsMxChecker, type MxResolver, NodeDnsMxResolver} from './adapters/DnsMxChecker';
import {createDomainAgeChecker} from './adapters/DomainAgeChecker';
@@ -13,13 +14,12 @@ import {analyzeRegistrationTiming} from './adapters/RegistrationTimingAnalyzer';
import {analyzeUserAgent} from './adapters/UserAgentAnalyzer';
import {createVelocityAdapter, type IRegistrationEventsRepository} from './adapters/VelocityAdapter';
import type {IRiskHistoryRepository} from './HistoricalOutcomeRepository';
import type {ReadonlyRiskCacheRef} from './RiskCacheManager';
import type {RiskToolbox} from './RiskToolbox';
import type {IpInfoAnonymousResult, ReverseDnsResult} from './RiskTypes';
import type {ISuspiciousIpRepository} from './SuspiciousIpRepository';
interface RiskToolboxFactoryOptions {
disposableDomainsRef: ReadonlyRiskCacheRef<ReadonlySet<string>>;
adminRepository: Pick<IAdminRepository, 'isEmailDomainSuspicious' | 'isEmailDomainDisposable'>;
ipInfoChecker?: (ip: string) => Promise<IpInfoAnonymousResult>;
reverseDnsLookup?: (ip: string) => Promise<ReverseDnsResult>;
ipInfoService: IpInfoService;
@@ -32,7 +32,7 @@ interface RiskToolboxFactoryOptions {
}
export function createRiskToolbox(opts: RiskToolboxFactoryOptions): RiskToolbox {
const checkDomainDisposable = createDisposableDomainChecker({disposableDomainsRef: opts.disposableDomainsRef});
const checkDomainDisposable = createDisposableDomainChecker({adminRepository: opts.adminRepository});
const lookupGeoIpCity = createGeoIpCityAdapter({ipInfoService: opts.ipInfoService});
const lookupGeoIpAsn = createGeoIpAsnAdapter({ipInfoService: opts.ipInfoService});
const checkMx = createDnsMxChecker({
-1
View File
@@ -97,7 +97,6 @@ export interface EmailSyntaxResult {
export interface DisposableCheckResult {
domain: string;
isDisposable: boolean;
listSize: number;
}
export interface MxCheckResult {
@@ -54,7 +54,6 @@ function createToolbox(
checkDomainDisposable: async ({domain}) => ({
domain,
isDisposable: false,
listSize: 0,
}),
checkMx: async ({domain}) => ({
domain,
@@ -1,21 +1,22 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {IAdminRepository} from '../../admin/IAdminRepository';
import type {DisposableCheckResult} from '../RiskTypes';
interface DisposableDomainCheckerContext {
disposableDomainsRef: {
readonly current: ReadonlySet<string>;
};
adminRepository: Pick<IAdminRepository, 'isEmailDomainSuspicious' | 'isEmailDomainDisposable'>;
}
export function createDisposableDomainChecker(ctx: DisposableDomainCheckerContext) {
return async function checkDomainDisposable(args: {domain: string}): Promise<DisposableCheckResult> {
const domain = args.domain.toLowerCase().trim();
const set = ctx.disposableDomainsRef.current;
const [suspicious, disposable] = await Promise.all([
ctx.adminRepository.isEmailDomainSuspicious(domain),
ctx.adminRepository.isEmailDomainDisposable(domain),
]);
return {
domain,
isDisposable: set.has(domain),
listSize: set.size,
isDisposable: suspicious || disposable,
};
};
}
+10 -2
View File
@@ -11,6 +11,7 @@ interface CronDefinition {
taskType: WorkerTaskName;
payload: WorkerJobPayload;
cronExpression: string;
ledger: boolean;
lastFired: number;
}
@@ -89,12 +90,19 @@ export class CronScheduler {
this.kvClient = kvClient;
}
upsert(id: string, taskType: WorkerTaskName, payload: WorkerJobPayload, cronExpression: string): void {
upsert(
id: string,
taskType: WorkerTaskName,
payload: WorkerJobPayload,
cronExpression: string,
options: {ledger: boolean},
): void {
this.definitions.set(id, {
id,
taskType,
payload,
cronExpression,
ledger: options.ledger,
lastFired: 0,
});
}
@@ -133,7 +141,7 @@ export class CronScheduler {
if (!acquired) {
continue;
}
await this.workerService.addJob(def.taskType, def.payload, {jobKey});
await this.workerService.addJob(def.taskType, def.payload, {jobKey, skipLedger: !def.ledger});
this.logger.debug({cronId: def.id, taskType: def.taskType}, 'Cron job fired');
} catch (error) {
this.logger.error({err: error, cronId: def.id, taskType: def.taskType}, 'Failed to enqueue cron job');
+31 -19
View File
@@ -41,21 +41,27 @@ const SEARCH_REQUIRED_TASKS = new Set<string>([
]);
function registerCronJobs(cron: CronScheduler): void {
cron.upsert('processAssetDeletionQueue', 'processAssetDeletionQueue', {}, '0 */5 * * * *');
cron.upsert('processBunnyPurgeQueue', 'processBunnyPurgeQueue', {}, '*/10 * * * * *');
cron.upsert('processPendingBulkMessageDeletions', 'processPendingBulkMessageDeletions', {}, '0 */10 * * * *');
cron.upsert('userProcessPendingDeletions', 'userProcessPendingDeletions', {}, '0 * * * * *');
cron.upsert('processPremiumStateReconciliationQueue', 'processPremiumStateReconciliationQueue', {}, '0 * * * * *');
cron.upsert('processExpiredPremiumSweep', 'processExpiredPremiumSweep', {}, '0 0 * * * *');
cron.upsert('processInactivityDeletions', 'processInactivityDeletions', {}, '0 0 */6 * * *');
cron.upsert('expireAttachments', 'expireAttachments', {}, '0 0 */12 * * *');
cron.upsert('prunePostgresKvTtl', 'prunePostgresKvTtl', {}, '0 */5 * * * *');
cron.upsert('syncDiscoveryIndex', 'syncDiscoveryIndex', {}, '0 */15 * * * *');
cron.upsert('syncDisposableEmailDomains', 'syncDisposableEmailDomains', {}, '0 */30 * * * *');
cron.upsert('syncUrlBlocklists', 'syncUrlBlocklists', {}, '0 0 */6 * * *');
cron.upsert('syncFileShaBlocklists', 'syncFileShaBlocklists', {}, '0 0 */12 * * *');
cron.upsert('flushUserActivityBuffer', 'flushUserActivityBuffer', {}, '*/10 * * * * *');
Logger.info('Cron jobs registered successfully');
cron.upsert('processAssetDeletionQueue', 'processAssetDeletionQueue', {}, '0 */5 * * * *', {ledger: false});
cron.upsert('processBunnyPurgeQueue', 'processBunnyPurgeQueue', {}, '*/10 * * * * *', {ledger: false});
cron.upsert('processPendingBulkMessageDeletions', 'processPendingBulkMessageDeletions', {}, '0 */10 * * * *', {
ledger: false,
});
cron.upsert('userProcessPendingDeletions', 'userProcessPendingDeletions', {}, '0 * * * * *', {ledger: false});
cron.upsert('processPremiumStateReconciliationQueue', 'processPremiumStateReconciliationQueue', {}, '0 * * * * *', {
ledger: false,
});
cron.upsert('processExpiredPremiumSweep', 'processExpiredPremiumSweep', {}, '0 0 * * * *', {ledger: false});
cron.upsert('processInactivityDeletions', 'processInactivityDeletions', {}, '0 0 */6 * * *', {ledger: false});
cron.upsert('expireAttachments', 'expireAttachments', {}, '0 0 */12 * * *', {ledger: false});
cron.upsert('prunePostgresKvTtl', 'prunePostgresKvTtl', {}, '0 */5 * * * *', {ledger: false});
cron.upsert('syncDiscoveryIndex', 'syncDiscoveryIndex', {}, '0 */15 * * * *', {ledger: false});
if (Config.blocklistFeeds.enabled) {
cron.upsert('syncDisposableEmailDomains', 'syncDisposableEmailDomains', {}, '0 0 */6 * * *', {ledger: true});
cron.upsert('syncUrlBlocklists', 'syncUrlBlocklists', {}, '0 0 */6 * * *', {ledger: true});
cron.upsert('syncFileShaBlocklists', 'syncFileShaBlocklists', {}, '0 0 */12 * * *', {ledger: true});
}
cron.upsert('flushUserActivityBuffer', 'flushUserActivityBuffer', {}, '*/10 * * * * *', {ledger: false});
Logger.info({blocklistFeeds: Config.blocklistFeeds.enabled}, 'Cron jobs registered successfully');
}
function workerLanesRequireSearch(activeWorkerLanes: ReadonlyArray<WorkerLaneDefinition>): boolean {
@@ -191,10 +197,16 @@ export async function startWorkerMain(): Promise<void> {
setInjectedWorkerService(workerService);
dependencies = await initializeWorkerDependencies(snowflakeService);
setWorkerDependencies(dependencies);
const didClaimEmailSync = await dependencies.kvClient.setnx('sync:email_domains:initialized', '1');
if (didClaimEmailSync) {
Logger.info('Triggering initial disposable email domain sync');
await workerService.addJob('syncDisposableEmailDomains', {});
if (Config.blocklistFeeds.enabled) {
const didClaimEmailSync = await dependencies.kvClient.setnx(
'sync:email_domains:initialized',
'1',
ms('6 hours') / 1000,
);
if (didClaimEmailSync) {
Logger.info('Triggering initial disposable email domain sync');
await workerService.addJob('syncDisposableEmailDomains', {});
}
}
cron = new CronScheduler(workerService, Logger, dependencies.kvClient);
registerCronJobs(cron);
@@ -21,7 +21,6 @@ const SOURCES = [
'https://raw.githubusercontent.com/vrittech/disposable-email/main/disposable_domains.txt',
'https://raw.githubusercontent.com/martenson/disposable-email-domains/master/disposable_email_blocklist.conf',
];
const CURRENT_DOMAIN_PAGE_SIZE = 10000;
const WRITE_PROGRESS_INTERVAL = 500;
const DOMAIN_REGEX = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
@@ -120,16 +119,7 @@ function normaliseDomain(raw: string): string | null {
async function loadCurrentDisposableEmailDomains(): Promise<Set<string>> {
const {adminRepository} = getWorkerDependencies();
const currentSet = new Set<string>();
let pageState: string | null = null;
do {
const page = await adminRepository.listDisposableEmailDomainsPage(CURRENT_DOMAIN_PAGE_SIZE, pageState);
for (const domain of page.domains) {
currentSet.add(domain);
}
pageState = page.pageState;
} while (pageState !== null);
return currentSet;
return new Set(await adminRepository.listDisposableEmailDomains());
}
async function throwIfCancelled(helpers: WorkerTaskHelpers): Promise<void> {
+7 -7
View File
@@ -327,7 +327,7 @@ const CONFIG: Config = {
{name: '--guild-list-foreground', position: 0.38},
{name: '--background-header-secondary', position: 0.5},
{name: '--background-header-primary', position: 0.5},
{name: '--background-textarea', position: 0.68},
{name: '--background-textarea', position: 0.3},
{name: '--background-header-primary-hover', position: 0.85},
],
},
@@ -344,7 +344,7 @@ const CONFIG: Config = {
{name: '--guild-list-foreground', position: 0.38},
{name: '--background-header-secondary', position: 0.5},
{name: '--background-header-primary', position: 0.5},
{name: '--background-textarea', position: 0.68},
{name: '--background-textarea', position: 0.3},
{name: '--background-header-primary-hover', position: 0.85},
],
},
@@ -410,7 +410,7 @@ const CONFIG: Config = {
{name: '--guild-list-foreground', position: 0.38},
{name: '--background-header-secondary', position: 0.5},
{name: '--background-header-primary', position: 0.5},
{name: '--background-textarea', position: 0.68},
{name: '--background-textarea', position: 0.3},
{name: '--background-header-primary-hover', position: 0.85},
],
},
@@ -438,8 +438,8 @@ const CONFIG: Config = {
name: '--panel-control-bg',
value: `color-mix(
in srgb,
var(--background-secondary-alt) 80%,
hsl(258, calc(10% * var(--saturation-factor)), 2%) 20%
var(--background-secondary-alt) 90%,
hsl(258, calc(10% * var(--saturation-factor)), 2%) 10%
)`,
},
{name: '--panel-control-border', family: 'neutralDark', saturation: 30, lightness: 65, alpha: 0.45},
@@ -744,8 +744,8 @@ hsl(258, calc(10% * var(--saturation-factor)), 0%) 10%
name: '--panel-control-bg',
value: `color-mix(
in srgb,
var(--background-secondary-alt) 80%,
hsl(220, calc(13% * var(--saturation-factor)), 2%) 20%
var(--background-secondary-alt) 90%,
hsl(220, calc(13% * var(--saturation-factor)), 2%) 10%
)`,
},
{name: '--panel-control-border', family: 'legacyDark', saturation: 30, lightness: 65, alpha: 0.45},
+52 -10
View File
@@ -238,21 +238,63 @@ function readSourceVariables(appDir: string): {
return {darkDefaults, lightDefaults, sources};
}
function findMatchingParen(text: string, openIndex: number): number {
let depth = 0;
for (let index = openIndex; index < text.length; index += 1) {
const character = text[index];
if (character === '(') depth += 1;
if (character === ')') {
depth -= 1;
if (depth === 0) return index;
}
}
return -1;
}
function splitVarArguments(inner: string): {dependency: string; fallback: string | null} {
let depth = 0;
for (let index = 0; index < inner.length; index += 1) {
const character = inner[index];
if (character === '(') depth += 1;
if (character === ')') depth -= 1;
if (character === ',' && depth === 0) {
return {dependency: inner.slice(0, index).trim(), fallback: inner.slice(index + 1).trim()};
}
}
return {dependency: inner.trim(), fallback: null};
}
function resolveVariableValue(name: string, values: ReadonlyMap<string, string>, stack = new Set<string>()): string {
const value = values.get(name);
if (!value) return '';
return value.replace(
/var\(\s*(--[a-zA-Z0-9_-]+)(?:\s*,\s*([^)]+))?\)/g,
(full, dependency: string, fallback?: string) => {
if (dependency === '--saturation-factor') return full;
if (stack.has(dependency)) return fallback?.trim() ?? full;
const dependencyValue = values.get(dependency);
if (!dependencyValue) return fallback?.trim() ?? full;
let result = '';
let cursor = 0;
while (cursor < value.length) {
const start = value.indexOf('var(', cursor);
if (start === -1) {
result += value.slice(cursor);
return result;
}
const close = findMatchingParen(value, start + 3);
if (close === -1) {
result += value.slice(cursor);
return result;
}
result += value.slice(cursor, start);
const full = value.slice(start, close + 1);
const {dependency, fallback} = splitVarArguments(value.slice(start + 4, close));
if (dependency === '--saturation-factor') {
result += full;
} else if (stack.has(dependency) || !values.get(dependency)) {
result += fallback ?? full;
} else {
const nextStack = new Set(stack);
nextStack.add(name);
return resolveVariableValue(dependency, values, nextStack);
},
);
result += resolveVariableValue(dependency, values, nextStack);
}
cursor = close + 1;
}
return result;
}
function getGroupId(name: string): string {
+38 -13
View File
@@ -98,13 +98,37 @@ var {
--media-border-radius: 0.25rem;
--input-container-padding: 0.625rem;
--footer-row-height: 4.5rem;
--input-container-min-height: var(--footer-row-height);
--list-row-min-height: 4.5rem;
--input-wrapper-padding-x: 0.5rem;
--input-wrapper-padding-bottom: 0.5rem;
--textarea-top-bar-height: 2.5rem;
--textarea-line-height: 1.375rem;
--textarea-content-offset: calc((var(--user-area-content-height) - var(--textarea-line-height)) / 2);
--footer-box-height: 3.625rem;
--footer-box-inset: 0.375rem;
--footer-box-inset-inline: var(--footer-box-inset);
--footer-box-radius: var(--radius-lg);
--footer-box-inner-inset: 0.5rem;
--outline-frame-border-width: 0.0625rem;
--composer-mobile-box-height: 3rem;
--composer-action-gap: 0.25rem;
--floating-surface-ring-color: color-mix(in srgb, var(--background-modifier-accent) 20%, transparent);
--floating-surface-ring-color-strong: color-mix(in srgb, var(--background-modifier-accent) 45%, transparent);
--footer-box-padding-y: max(0rem, calc((var(--footer-box-height) - var(--textarea-button-height, 2rem)) / 2));
--composer-mobile-padding-y: max(
0rem,
calc((var(--composer-mobile-box-height) - var(--textarea-button-height, 2rem)) / 2)
);
--footer-row-height: calc(var(--footer-box-height) + var(--footer-box-inset) * 2);
--input-container-min-height: var(--footer-row-height);
--textarea-min-height: var(--footer-box-height);
--textarea-padding-y: var(--footer-box-padding-y);
--composer-box-inset: max(0rem, calc((var(--input-container-min-height) - var(--textarea-min-height)) / 2));
--composer-box-inset-inline: min(var(--footer-box-inset), var(--chat-horizontal-padding, var(--spacing-4)));
--composer-box-padding-inline: max(
0rem,
calc(var(--chat-horizontal-padding, var(--spacing-4)) - var(--composer-box-inset-inline))
);
--typing-indicator-height: 1rem;
--typing-pill-height: 1rem;
@@ -113,10 +137,6 @@ var {
--typing-avatar-size: 0.75rem;
--typing-indicator-animation-size: 1rem;
--typing-indicator-gap: 0px;
--typing-upload-column-width: calc(
var(--user-area-content-height) +
(var(--textarea-side-button-padding, 0.34375rem) * 2)
);
--spoiler-border-radius: 0.375rem;
--markup-restricted-inline-icon-baseline-shift: -0.125em;
@@ -145,14 +165,19 @@ var {
--layout-guild-list-width: 4.5rem;
--layout-sidebar-width: 20rem;
--layout-header-height: 3.5rem;
--layout-user-area-height: var(--input-container-min-height);
--layout-user-area-height: var(--footer-box-height);
--layout-user-area-reserved-height: 0px;
--layout-mobile-bottom-nav-reserved-height: 0px;
--user-area-content-height: 2.25rem;
--user-area-padding-y: calc((var(--layout-user-area-height) - var(--user-area-content-height)) / 2);
--user-area-padding-x: var(--spacing-4);
--user-area-box-inset-block-end: calc(var(--footer-box-inset) + var(--outline-frame-border-width));
--user-area-content-height: var(--textarea-button-height, 2rem);
--user-area-padding-y: var(--footer-box-padding-y);
--user-area-padding-x: var(--footer-box-inner-inset);
--user-area-avatar-lead: max(
var(--footer-box-inner-inset),
calc(var(--layout-guild-list-width) / 2 - var(--footer-box-inset-inline) - var(--user-area-content-height) / 2)
);
--voice-connection-padding-y: var(--spacing-2);
--footer-row-padding-y: var(--user-area-padding-y);
--voice-connection-padding-x: var(--footer-box-inner-inset);
--layout-header-popout-width: calc(var(--layout-sidebar-width) - (var(--spacing-4) * 2));
--layout-gap: var(--spacing-4);
@@ -19,7 +19,11 @@
}
.guildsLayoutReserveSpace {
--layout-user-area-overlay-height: calc(var(--layout-user-area-height) + var(--layout-voice-connection-height, 0px));
--layout-user-area-overlay-height: calc(
var(--layout-user-area-height) +
var(--user-area-box-inset-block-end) +
var(--layout-voice-connection-height, 0px)
);
}
.guildsLayoutReserveMobileBottomNav {
@@ -181,7 +185,8 @@
width: calc(var(--layout-guild-list-width) + var(--layout-sidebar-width) + 0.0625rem);
display: flex;
align-items: flex-end;
padding: 0;
padding: 0 var(--footer-box-inset-inline) var(--user-area-box-inset-block-end);
background-color: var(--background-secondary);
pointer-events: none;
z-index: var(--z-index-elevated-1);
}
@@ -4,7 +4,7 @@
position: relative;
height: 100%;
width: 100%;
border: 0.0625rem solid var(--user-area-divider-color);
border: var(--outline-frame-border-width, 0.0625rem) solid var(--user-area-divider-color);
border-top: none;
border-top-left-radius: var(--outline-radius, 0px);
background: var(--background-secondary-lighter);
@@ -6,20 +6,19 @@
gap: 0;
width: 100%;
background-color: var(--panel-control-bg);
border-radius: var(--footer-box-radius);
box-shadow: inset 0 0 0 0.0625rem var(--floating-surface-ring-color);
overflow: hidden;
position: relative;
}
.separator {
height: 0.0625rem;
background-color: var(--user-area-divider-color);
}
.userAreaContainer {
display: flex;
align-items: center;
gap: var(--spacing-3);
margin: 0;
padding: var(--user-area-padding-y) var(--user-area-padding-x);
padding-block: var(--user-area-padding-y);
padding-inline: var(--user-area-avatar-lead) var(--user-area-padding-x);
box-sizing: border-box;
background-color: transparent;
width: 100%;
@@ -30,17 +29,8 @@
--popout-radius: 0.375rem;
}
.userAreaInnerWrapperHasVoiceConnection {
min-height: var(--layout-user-area-height);
}
.userAreaInnerWrapperHasVoiceConnection .userAreaContainer {
border-top: 0;
}
.voiceConnectionWrapper {
border-bottom: 0;
border-top: 0;
border-bottom: 0.0625rem solid var(--floating-surface-ring-color-strong);
}
.userInfo {
@@ -50,11 +40,12 @@
flex: 1;
min-width: 0;
cursor: pointer;
padding: 0 var(--spacing-2);
padding: 0;
margin: 0;
border-radius: var(--radius-md);
height: var(--user-area-content-height);
position: relative;
isolation: isolate;
transition: color var(--transition-normal);
outline: none;
}
@@ -98,7 +89,7 @@
.userStatus {
font-size: 0.6875rem;
line-height: 1rem;
line-height: 0.875rem;
color: color-mix(in srgb, var(--text-primary-muted) 85%, transparent);
font-weight: 500;
overflow: hidden;
@@ -200,10 +200,7 @@ const UserAreaInner = observer(
clearHeight();
};
}, [hasVoiceConnection]);
const wrapperClassName = clsx(
styles.userAreaInnerWrapper,
hasVoiceConnection && styles.userAreaInnerWrapperHasVoiceConnection,
);
const wrapperClassName = styles.userAreaInnerWrapper;
const pushToTalkCombo = Keybind.getByAction('voice_push_to_talk').combo;
const pushToTalkHint = formatKeyCombo(pushToTalkCombo);
const isPushToTalkEffective = Keybind.isPushToTalkEffective();
@@ -251,19 +248,14 @@ const UserAreaInner = observer(
>
{hasVoiceConnection && (
<div ref={voiceConnectionRef} data-flx="app.user-area.user-area-inner.div">
<div className={styles.separator} aria-hidden data-flx="app.user-area.user-area-inner.separator" />
<div
className={styles.voiceConnectionWrapper}
data-flx="app.user-area.user-area-inner.voice-connection-wrapper"
>
<VoiceConnectionStatus data-flx="app.user-area.user-area-inner.voice-connection-status" />
</div>
<div className={styles.separator} aria-hidden data-flx="app.user-area.user-area-inner.separator--2" />
</div>
)}
{!hasVoiceConnection && (
<div className={styles.separator} aria-hidden data-flx="app.user-area.user-area-inner.separator--3" />
)}
<div className={styles.userAreaContainer} data-flx="app.user-area.user-area-inner.user-area-container">
<Popout
data-flx="app.user-area.user-area-inner.popout"
@@ -283,7 +275,7 @@ const UserAreaInner = observer(
tabIndex={0}
data-flx="app.user-area.user-area-inner.user-info"
>
<StatusAwareAvatar user={user} size={36} data-flx="app.user-area.user-area-inner.status-aware-avatar" />
<StatusAwareAvatar user={user} size={32} data-flx="app.user-area.user-area-inner.status-aware-avatar" />
<div className={styles.userInfoText} data-flx="app.user-area.user-area-inner.user-info-text">
<div className={styles.userName} data-flx="app.user-area.user-area-inner.user-name">
{displayName}
@@ -11,6 +11,7 @@ import {
YOU_CAN_T_UNMUTE_YOURSELF_BECAUSE_A_MODERATOR_DESCRIPTOR,
} from '@app/features/app/keybindings/keybind_manager/shared';
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import {requestChannelComposerAffordanceDismissal} from '@app/features/channel/components/ChannelComposerDismissal';
import {CreateDMModal} from '@app/features/channel/components/modals/CreateDMModal';
import Channels from '@app/features/channel/state/Channels';
import * as VoiceStateCommands from '@app/features/devtools/commands/VoiceStateCommands';
@@ -325,8 +326,9 @@ export function registerDefaultKeybindHandlers(host: HandlerHost, i18n: I18n): v
if (type !== 'press') return;
const channelId = host.currentChannelId;
if (!channelId) return;
if (requestChannelComposerAffordanceDismissal(channelId)) return;
if (ReadStates.hasUnread(channelId)) {
ComponentDispatch.dispatch('ESCAPE_PRESSED');
ComponentDispatch.dispatch('ESCAPE_PRESSED', {channelId});
}
});
host.register('chat_mark_guild_read', ({type}) => {
@@ -164,6 +164,9 @@ export {
isSpecialMention,
isSticker,
} from '@app/features/channel/components/AutocompleteTypes';
const ATTACHED_AUTOCOMPLETE_GAP = 4;
export const Autocomplete = observer(
({
type,
@@ -214,11 +217,11 @@ export const Autocomplete = observer(
setReferenceState(resolveReferenceElement(referenceElement));
}, [referenceElement]);
const portalHost = usePortalHost();
let resolvedMainAxisOffset = attached ? 0 : 8;
let resolvedMainAxisOffset = attached ? ATTACHED_AUTOCOMPLETE_GAP : 8;
if (mainAxisOffset != null) {
resolvedMainAxisOffset = mainAxisOffset;
}
const resolvedCrossAxisOffset = attached ? 8 : 0;
const resolvedCrossAxisOffset = 0;
const heading = resolveAutocompleteHeading(type, options, i18n);
const {refs, floatingStyles} = useFloating({
placement: 'top-start',
@@ -230,7 +233,7 @@ export const Autocomplete = observer(
flip({padding: 16}),
size({
apply({rects, elements}) {
const width = attached ? Math.max(0, rects.reference.width - 16) : rects.reference.width;
const width = rects.reference.width;
Object.assign(elements.floating.style, {
width: `${width}px`,
});
@@ -1,6 +1,15 @@
/* SPDX-License-Identifier: AGPL-3.0-or-later */
.container {
--composer-surface-color: var(--background-secondary-lighter);
--composer-status-line-height: 1.125rem;
--composer-status-safe-gap: 0.5rem;
--composer-status-safe-area: calc(
var(--composer-status-line-height) +
var(--composer-status-safe-gap) +
var(--composer-status-safe-gap)
);
--messages-bottom-clearance: max(0px, calc(var(--composer-status-safe-area) - var(--composer-box-inset, 0px)));
display: flex;
flex-direction: column;
height: 100%;
@@ -9,7 +18,7 @@
max-width: 100%;
min-width: 0;
overflow: hidden;
background-color: var(--background-secondary-lighter);
background-color: var(--composer-surface-color);
contain: layout style;
}
@@ -33,7 +42,6 @@
z-index: 1;
padding: 0;
overflow: visible;
background-color: var(--background-secondary-lighter);
width: 100%;
max-width: 100%;
box-sizing: border-box;
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {ComponentDispatch} from '@app/features/platform/utils/ComponentBus';
export interface ChannelComposerDismissalRequest {
channelId: string;
}
export function requestChannelComposerAffordanceDismissal(channelId: string): boolean {
const result = ComponentDispatch.dispatchToFirstResult(
'TEXTAREA_DISMISS_AFFORDANCE',
{channelId},
(candidate) => candidate === true,
);
return result === true;
}
@@ -7,7 +7,6 @@ import {TypingUsers, usePresentableTypingUsers} from '@app/features/channel/comp
import wrapperStyles from '@app/features/channel/components/textarea/InputWrapper.module.css';
import type {Channel} from '@app/features/channel/models/Channel';
import type {Message} from '@app/features/messaging/models/MessagingMessage';
import {clsx} from 'clsx';
import {observer} from 'mobx-react-lite';
import type React from 'react';
@@ -24,7 +23,6 @@ interface ChannelComposerStatusRailProps {
slowmodeRemaining: number;
slowmodeImmune: boolean;
mobile: boolean;
leadingContentInFlow: boolean;
onCancelEdit: () => void;
}
@@ -41,7 +39,6 @@ export const ChannelComposerStatusRail = observer(function ChannelComposerStatus
slowmodeRemaining,
slowmodeImmune,
mobile,
leadingContentInFlow,
onCancelEdit,
}: ChannelComposerStatusRailProps) {
const presentableTypingUsers = usePresentableTypingUsers(channel);
@@ -50,9 +47,6 @@ export const ChannelComposerStatusRail = observer(function ChannelComposerStatus
const leadingContentVisible = mobileEditVisible || replyVisible;
const typingVisible = showTypingStatus && !autocompleteVisible && presentableTypingUsers.length > 0;
const slowmodeVisible = showSlowmodeStatus && slowmodeEnabled;
if (!leadingContentVisible && !typingVisible && !slowmodeVisible) {
return null;
}
let topBar: React.ReactNode = null;
if (mobileEditVisible) {
topBar = <EditBar channel={channel} onCancel={onCancelEdit} data-flx="channel.composer-status-rail.edit-bar" />;
@@ -68,35 +62,33 @@ export const ChannelComposerStatusRail = observer(function ChannelComposerStatus
);
}
return (
<div
className={clsx(wrapperStyles.statusRail, leadingContentInFlow && wrapperStyles.statusRailInFlow)}
data-flx="channel.composer-status-rail.container"
>
<div
className={clsx(wrapperStyles.statusRailLeft, leadingContentVisible && wrapperStyles.statusRailLeftWithLeading)}
data-flx="channel.composer-status-rail.left"
>
{topBar !== null && (
<div className={wrapperStyles.topBarContainer} data-flx="channel.composer-status-rail.top-bar">
{topBar}
</div>
)}
{typingVisible && (
<div className={wrapperStyles.statusTypingSlot} data-flx="channel.composer-status-rail.typing-slot">
<TypingUsers channel={channel} data-flx="channel.composer-status-rail.typing-users" />
<>
<div className={wrapperStyles.statusRail} data-flx="channel.composer-status-rail.container">
<div className={wrapperStyles.statusRailLeft} data-flx="channel.composer-status-rail.left">
{typingVisible && (
<div className={wrapperStyles.statusTypingSlot} data-flx="channel.composer-status-rail.typing-slot">
<TypingUsers channel={channel} showAvatars={true} data-flx="channel.composer-status-rail.typing-users" />
</div>
)}
</div>
{slowmodeVisible && (
<div className={wrapperStyles.statusSlowmodeSlot} data-flx="channel.composer-status-rail.slowmode-slot">
<SlowmodeIndicator
slowmodeRemaining={slowmodeRemaining}
slowmodeDuration={channel.rateLimitPerUser * 1000}
isImmune={slowmodeImmune}
data-flx="channel.composer-status-rail.slowmode-indicator"
/>
</div>
)}
</div>
{slowmodeVisible && (
<div className={wrapperStyles.statusSlowmodeSlot} data-flx="channel.composer-status-rail.slowmode-slot">
<SlowmodeIndicator
slowmodeRemaining={slowmodeRemaining}
slowmodeDuration={channel.rateLimitPerUser * 1000}
isImmune={slowmodeImmune}
data-flx="channel.composer-status-rail.slowmode-indicator"
/>
{leadingContentVisible && (
<div className={wrapperStyles.composerActionStack} data-flx="channel.composer-status-rail.action-stack">
<div className={wrapperStyles.composerActionRow} data-flx="channel.composer-status-rail.action-row">
{topBar}
</div>
</div>
)}
</div>
</>
);
});
@@ -23,20 +23,16 @@
flex-shrink: 0;
border: none;
background-color: transparent;
width: var(--composer-status-pill-height, 1.5rem);
height: var(--composer-status-pill-height, 1.5rem);
width: var(--composer-action-button-size, 2rem);
height: var(--composer-action-button-size, 2rem);
padding: 0;
border-radius: var(--radius-full);
color: var(--text-primary-muted);
line-height: 0;
transition:
color 200ms,
background-color 200ms;
transition: color 200ms;
}
.button:hover {
color: var(--text-primary);
background-color: var(--background-modifier-hover);
}
.icon {
@@ -34,7 +34,7 @@
.scrollerSpacer {
display: block;
height: var(--scroller-spacer-height);
height: var(--messages-bottom-clearance, var(--scroller-spacer-height));
pointer-events: none;
width: 100%;
flex-shrink: 0;
@@ -147,7 +147,7 @@
.messageBottomPill {
top: auto;
bottom: 0.625rem;
bottom: calc(0.625rem + var(--composer-status-safe-area, 0px));
}
.jumpToPresentBar {
@@ -324,12 +324,17 @@ export const Messages = observer(function Messages({
ReadStateCommands.ack(channel.id, true, false);
}
}, [channel.id, state.messages?.hasMoreAfter, state.visualUnreadMessageId, scrollManager]);
const onEscapePressed = useCallback(() => {
if (scrollManager.jumpReturnToOrigin()) {
return;
}
onScrollToPresentAndAck();
}, [onScrollToPresentAndAck, scrollManager]);
const onEscapePressed = useCallback(
(payload?: unknown) => {
const data = payload as {channelId?: string} | undefined;
if (data?.channelId && data.channelId !== channel.id) return;
if (scrollManager.jumpReturnToOrigin()) {
return;
}
onScrollToPresentAndAck();
},
[channel.id, onScrollToPresentAndAck, scrollManager],
);
const onRetryLoadMessages = useCallback(() => {
void MessageCommands.fetchMessages(channel.id, null, null, MAX_MESSAGES_PER_CHANNEL);
}, [channel.id]);
@@ -11,8 +11,9 @@
.replyTargetButton {
display: block;
justify-self: start;
min-width: 0;
width: 100%;
max-width: 100%;
cursor: pointer;
border: 0;
background: transparent;
@@ -35,7 +36,7 @@
align-items: center;
flex: 0 0 auto;
gap: 0.125rem;
min-height: var(--composer-status-pill-height, 1.5rem);
min-height: var(--composer-action-button-size, 2rem);
}
.mentionToggle {
@@ -43,7 +44,7 @@
cursor: pointer;
align-items: center;
justify-content: center;
height: calc(var(--composer-status-pill-height, 1.5rem) - 0.25rem);
height: calc(var(--composer-action-button-size, 2rem) - 0.25rem);
inline-size: 3.5rem;
flex: 0 0 3.5rem;
padding: 0 0.5rem;
@@ -91,20 +92,16 @@
flex-shrink: 0;
border: none;
background-color: transparent;
width: var(--composer-status-pill-height, 1.5rem);
height: var(--composer-status-pill-height, 1.5rem);
width: var(--composer-action-button-size, 2rem);
height: var(--composer-action-button-size, 2rem);
padding: 0;
border-radius: var(--radius-full);
color: var(--text-primary-muted);
line-height: 0;
transition:
color 200ms,
background-color 200ms;
transition: color 200ms;
}
.closeButton:hover {
color: var(--text-primary);
background-color: var(--background-modifier-hover);
}
.closeIcon {
@@ -1,8 +1,7 @@
/* SPDX-License-Identifier: AGPL-3.0-or-later */
.container {
border-color: var(--background-header-secondary);
background-color: var(--background-secondary-lighter);
border-color: var(--composer-divider-color, var(--background-modifier-accent));
padding: 0.5rem 1rem;
width: fit-content;
}
@@ -13,7 +12,7 @@
}
.standalone {
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
border-radius: var(--radius-lg);
border-width: 0.0625rem;
border-style: solid;
}
@@ -165,14 +165,13 @@ export const EditingMessageInput = observer(
ComponentDispatch.dispatch('INBOX_OPEN');
return;
}
if (event.key === 'Escape' && !event.shiftKey) {
if (event.key === 'Escape' && !event.shiftKey && !event.defaultPrevented && !event.nativeEvent.isComposing) {
event.preventDefault();
event.stopPropagation();
ComponentDispatch.dispatch('ESCAPE_PRESSED', {channelId: channel.id});
onCancel();
}
},
[actualContent, channel.id, onCancel],
[actualContent, onCancel],
);
useEffect(() => {
if (editingDisabled || hasFocusedInitiallyRef.current) {
@@ -7,6 +7,10 @@ import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import {Limits} from '@app/features/app/utils/UserLimits';
import {fetchSlowmodeState} from '@app/features/channel/commands/ChannelCommands';
import {ChannelAttachmentArea} from '@app/features/channel/components/ChannelAttachmentArea';
import {
type ChannelComposerDismissalRequest,
requestChannelComposerAffordanceDismissal,
} from '@app/features/channel/components/ChannelComposerDismissal';
import {EditBar} from '@app/features/channel/components/ChannelEditBar';
import {ReplyBar} from '@app/features/channel/components/ChannelReplyBar';
import {ChannelStickersArea} from '@app/features/channel/components/ChannelStickersArea';
@@ -99,7 +103,6 @@ import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import * as PopoutCommands from '@app/features/ui/commands/PopoutCommands';
import {SlashCommandIcon} from '@app/features/ui/components/icons/SlashCommandIcon';
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import {openPopout} from '@app/features/ui/popover/PopoverPopout';
import ContextMenuState from '@app/features/ui/state/ContextMenu';
import KeyboardMode from '@app/features/ui/state/KeyboardMode';
@@ -110,7 +113,7 @@ import {openVoiceMessageComposerModal} from '@app/features/voice/components/Voic
import {flxElementClassName} from '@app/lib/react';
import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/macro';
import {PlusCircleIcon} from '@phosphor-icons/react';
import {PlusIcon} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import {useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState} from 'react';
@@ -128,6 +131,8 @@ const CLEAR_COMMAND_DESCRIPTOR = msg({
comment: 'Accessible label for the composer button that clears the slash command currently being composed.',
});
const PLUS_ICON_PROPS = {weight: 'bold'} as const;
export const LexicalChannelTextareaContent = observer(
({
channel,
@@ -672,21 +677,88 @@ export const LexicalChannelTextareaContent = observer(
onMentionConfirmationNeeded: handleMentionConfirmationNeeded,
i18n: i18n,
});
const handleClearSlashCommand = useCallback(() => {
const handle = handleRef.current;
if (handle !== null) {
handle.clear();
}
setValue('');
clearSegments();
DraftCommands.deleteDraft(channel.id);
if (handle !== null) {
handle.focus();
}
}, [channel.id, clearSegments]);
const handleCancelEdit = useCallback(() => {
setValue('');
clearSegments();
}, [clearSegments]);
const focusComposer = useCallback(() => {
const handle = handleRef.current;
if (handle !== null) {
handle.focus();
}
}, []);
const dismissTopmostComposerAffordance = useCallback((): boolean => {
const editingInline = MessageEdit.getEditingMessageId(channel.id) !== null;
if (editingInline) {
MessageCommands.stopEdit(channel.id);
return true;
}
if (hasPendingSticker) {
ChannelSticker.removePendingSticker(channel.id);
focusComposer();
return true;
}
if (hasAttachments) {
CloudUpload.clearTextarea(channel.id);
focusComposer();
return true;
}
const slashCommandResolution = LexicalMessageCommandResolver.resolve(handleRef.current);
if (slashCommandResolution.status !== LexicalMessageCommandResolutionStatus.NO_COMMAND) {
handleClearSlashCommand();
return true;
}
if (editingMessageForComposer !== null && mobileLayout.enabled) {
MessageCommands.stopEditMobile(channel.id);
handleCancelEdit();
focusComposer();
return true;
}
if (replyingMessage !== null) {
MessageCommands.stopReply(channel.id);
focusComposer();
return true;
}
return false;
}, [
channel.id,
editingMessageForComposer,
focusComposer,
handleCancelEdit,
handleClearSlashCommand,
hasAttachments,
hasPendingSticker,
mobileLayout.enabled,
replyingMessage,
]);
useEffect(() => {
return ComponentDispatch.subscribe('TEXTAREA_DISMISS_AFFORDANCE', (request?: unknown) => {
const dismissalRequest = request as ChannelComposerDismissalRequest | undefined;
if (dismissalRequest?.channelId !== channel.id) {
return false;
}
return dismissTopmostComposerAffordance();
});
}, [channel.id, dismissTopmostComposerAffordance]);
const handleEscapeKey = useCallback(
(event: React.KeyboardEvent<HTMLElement>) => {
if (event.key !== 'Escape') return;
if (event.shiftKey) return;
if (hasAttachments || hasPendingSticker || replyingMessage) {
if (event.defaultPrevented || event.nativeEvent.isComposing) return;
if (event.key !== 'Escape' || event.shiftKey) return;
if (requestChannelComposerAffordanceDismissal(channel.id)) {
event.preventDefault();
if (hasAttachments) {
CloudUpload.clearTextarea(channel.id);
}
if (hasPendingSticker) {
ChannelSticker.removePendingSticker(channel.id);
}
if (replyingMessage) {
MessageCommands.stopReply(channel.id);
}
event.stopPropagation();
return;
}
if (isInputAreaFocused && KeyboardMode.keyboardModeEnabled) {
@@ -698,15 +770,7 @@ export const LexicalChannelTextareaContent = observer(
KeyboardMode.exitKeyboardMode();
}
},
[
channel.id,
hasAttachments,
hasPendingSticker,
replyingMessage,
isInputAreaFocused,
KeyboardMode.keyboardModeEnabled,
Accessibility.escapeExitsKeyboardMode,
],
[channel.id, isInputAreaFocused, KeyboardMode.keyboardModeEnabled, Accessibility.escapeExitsKeyboardMode],
);
const slotResolvers = useMemo<SlashSlotResolvers>(() => {
const guildId = channel.guildId;
@@ -776,18 +840,6 @@ export const LexicalChannelTextareaContent = observer(
}
MessageCommands.startEdit(channel.id, message.id, message.content);
}, [channel.id]);
const handleClearSlashCommand = useCallback(() => {
const handle = handleRef.current;
if (handle !== null) {
handle.clear();
}
setValue('');
clearSegments();
DraftCommands.deleteDraft(channel.id);
if (handle !== null) {
handle.focus();
}
}, [channel.id, clearSegments]);
useTextareaDraftAndTyping({
channelId: channel.id,
value,
@@ -808,11 +860,6 @@ export const LexicalChannelTextareaContent = observer(
textareaInputDisabled,
isFocused,
handleArrowUpEmpty,
editingMessage,
replyingMessage,
mobileLayout,
setValue,
clearSegments,
});
const messageLabel = i18n._(MESSAGE_DESCRIPTOR);
const messagePrefix = `${messageLabel} `;
@@ -933,10 +980,6 @@ export const LexicalChannelTextareaContent = observer(
resizeObserver.disconnect();
};
}, [mobileLayout.enabled]);
const handleCancelEdit = useCallback(() => {
setValue('');
clearSegments();
}, [clearSegments]);
const isPlusContextMenuOpen = useCallback(() => {
const plusButton = plusButtonRef.current;
const contextMenu = ContextMenuState.contextMenu;
@@ -1093,36 +1136,29 @@ export const LexicalChannelTextareaContent = observer(
],
);
const isSlashParamBarVisible = slashCommandState.activeSlot != null;
const hasStackedSections = Boolean(
referencedMessage ||
(editingMessage && mobileLayout.enabled) ||
uploadAttachments.length > 0 ||
hasPendingSticker ||
isSlashParamBarVisible,
);
const isMobileEditBarVisible = editingMessage && mobileLayout.enabled;
const isReplyBarVisible = !isMobileEditBarVisible && referencedMessage != null;
const presentableTypingUsers = usePresentableTypingUsers(channel);
const isTypingStatusVisible = !isAutocompleteVisible && presentableTypingUsers.length > 0;
const isSlowmodeIndicatorVisible = isSlowmodeEnabled;
const hasLeadingStatusContent = isMobileEditBarVisible || isReplyBarVisible || isSlashParamBarVisible;
const hasComposerStatusRail = isTypingStatusVisible || hasLeadingStatusContent || isSlowmodeIndicatorVisible;
let shouldReplyMention = false;
if (replyingMessage !== null && replyingMessage !== undefined) {
shouldReplyMention = replyingMessage.mentioning;
}
const topBarContent = isMobileEditBarVisible ? (
<EditBar channel={channel} onCancel={handleCancelEdit} />
) : (
referencedMessage && (
let topBarContent: React.ReactNode = null;
if (isMobileEditBarVisible) {
topBarContent = <EditBar channel={channel} onCancel={handleCancelEdit} />;
} else if (referencedMessage !== null) {
topBarContent = (
<ReplyBar
replyingMessageObject={referencedMessage}
shouldReplyMention={shouldReplyMention}
setShouldReplyMention={(mentioning) => MessageCommands.setReplyMentioning(channel.id, mentioning)}
channel={channel}
/>
)
);
);
}
const renderSection = (content: React.ReactNode, sectionClassName?: string) => (
<flx-channel-textarea-section className={flxElementClassName(wrapperStyles.stackSection, sectionClassName)}>
{content}
@@ -1130,171 +1166,155 @@ export const LexicalChannelTextareaContent = observer(
);
return (
<>
<FocusRing
focusTarget={editableRef}
ringTarget={containerRef}
offset={0}
enabled={!textareaInputDisabled && Accessibility.showTextareaFocusRing}
ringClassName={styles.textareaFocusRing}
<flx-channel-textarea
ref={containerRef}
className={flxElementClassName(
wrapperStyles.box,
wrapperStyles.composerRoot,
wrapperStyles.wrapperSides,
styles.textareaOuter,
mobileLayout.enabled && styles.textareaOuterMobile,
wrapperStyles.roundedAll,
textareaInputDisabled && wrapperStyles.disabled,
!mobileLayout.enabled && styles.textareaOuterRow,
)}
>
<flx-channel-textarea
ref={containerRef}
className={flxElementClassName(
wrapperStyles.box,
wrapperStyles.wrapperSides,
styles.textareaOuter,
mobileLayout.enabled && styles.textareaOuterMobile,
hasStackedSections ? wrapperStyles.roundedBottom : wrapperStyles.roundedAll,
wrapperStyles.bottomSpacing,
textareaInputDisabled && wrapperStyles.disabled,
!mobileLayout.enabled && styles.textareaOuterMinHeight,
)}
>
{hasComposerStatusRail && (
<flx-channel-textarea-status-rail className={flxElementClassName(wrapperStyles.statusRail)}>
<flx-channel-textarea-status-rail-left
className={flxElementClassName(
wrapperStyles.statusRailLeft,
hasLeadingStatusContent && wrapperStyles.statusRailLeftWithLeading,
)}
>
{topBarContent && (
<flx-channel-textarea-top-bar className={flxElementClassName(wrapperStyles.topBarContainer)}>
{topBarContent}
</flx-channel-textarea-top-bar>
)}
{slashCommandState.activeSlot != null && (
<flx-channel-textarea-slash-command-bar
className={flxElementClassName(wrapperStyles.topBarContainer)}
>
<SlashCommandParamBar
activeSlot={slashCommandState.activeSlot}
onClear={handleClearSlashCommand}
/>
</flx-channel-textarea-slash-command-bar>
)}
{isTypingStatusVisible && (
<flx-channel-textarea-typing-slot className={flxElementClassName(wrapperStyles.statusTypingSlot)}>
<TypingUsers channel={channel} withText={true} showAvatars={true} />
</flx-channel-textarea-typing-slot>
)}
</flx-channel-textarea-status-rail-left>
{isSlowmodeIndicatorVisible && (
<flx-channel-textarea-slowmode-slot className={flxElementClassName(wrapperStyles.statusSlowmodeSlot)}>
<SlowmodeIndicator
slowmodeRemaining={slowmodeRemaining}
slowmodeDuration={channel.rateLimitPerUser * 1000}
isImmune={isSlowmodeImmune}
/>
</flx-channel-textarea-slowmode-slot>
)}
</flx-channel-textarea-status-rail>
)}
{showAttachments &&
renderSection(<ChannelAttachmentArea channelId={channel.id} />, styles.collapsibleSection)}
{showStickers &&
renderSection(
<ChannelStickersArea channelId={channel.id} hasAttachments={hasAttachments} />,
styles.collapsibleSection,
<flx-channel-textarea-status-rail className={flxElementClassName(wrapperStyles.statusRail)}>
<flx-channel-textarea-status-rail-left className={flxElementClassName(wrapperStyles.statusRailLeft)}>
{isTypingStatusVisible && (
<flx-channel-textarea-typing-slot className={flxElementClassName(wrapperStyles.statusTypingSlot)}>
<TypingUsers channel={channel} withText={true} showAvatars={true} />
</flx-channel-textarea-typing-slot>
)}
{renderSection(
<flx-channel-textarea-box
className={flxElementClassName(
styles.mainWrapperDense,
textareaInputDisabled && wrapperStyles.disabled,
)}
>
<flx-channel-textarea-upload-column
className={flxElementClassName(styles.uploadButtonColumn, styles.sideButtonPadding)}
>
<TextareaButton
icon={slashCommandState.hasSlots ? SlashCommandIcon : PlusCircleIcon}
label={slashCommandState.hasSlots ? i18n._(CLEAR_COMMAND_DESCRIPTOR) : i18n._(OPEN_MENU_DESCRIPTOR)}
disabled={textareaInputDisabled}
aria-hidden={textareaInputDisabled ? true : undefined}
onMouseDown={slashCommandState.hasSlots ? undefined : handlePlusMenuMouseDown}
onClick={slashCommandState.hasSlots ? handleClearSlashCommand : handlePlusMenuClick}
forceHover={!slashCommandState.hasSlots && plusContextMenuOpen}
className={plusContextMenuOpen ? styles.plusButtonAboveBackdrop : undefined}
ref={plusButtonRef}
/>
</flx-channel-textarea-upload-column>
<flx-channel-textarea-content
ref={contentAreaRef}
className={flxElementClassName(styles.contentAreaDense)}
>
<flx-channel-textarea-composer className={flxElementClassName(lexicalStyles.composerHost)}>
<LexicalComposerInput
placeholder={placeholderText}
disabled={textareaInputDisabled}
handleRef={handleRef}
initialValue={initialDraftRef.current.display}
initialSegments={initialDraftRef.current.segments}
slotResolvers={slotResolvers}
emojiShortcodeResolver={composerEmojiResolver}
channelId={channel.id}
guildId={channel.guildId}
submitOnEnter={!mobileLayout.enabled}
className={lexicalStyles.composerEditable}
autocompleteOptions={autocompleteOptions}
autocompleteType={autocompleteType}
autocompleteQuery={autocompleteQuery}
autocompleteEnabled={!textareaInputDisabled}
slotMenuActive={isSlotMenu}
autocompleteReferenceElement={containerRef.current}
autocompleteListboxId={autocompleteListId}
onAutocompleteSelect={handleSelect}
onChange={handleEditorChange}
onCursorMove={onCursorMove}
onEnter={handleSubmit}
onArrowUp={handleArrowUpEmpty}
onKeyDown={handleEditorKeyDown}
onFocus={() => {
setIsFocused(true);
setIsInputAreaFocused(true);
ChannelSearch.setInputFocused(channel.id, false);
}}
onBlur={() => {
setIsFocused(false);
setIsInputAreaFocused(false);
}}
onSlashCommandStateChange={setSlashCommandState}
/>
</flx-channel-textarea-composer>
</flx-channel-textarea-content>
<TextareaButtons
disabled={textareaInputDisabled}
showAllButtons={showAllButtons}
showGifButton={showGifButton}
showMemesButton={showMemesButton}
showStickersButton={showStickersButton}
showEmojiButton={showEmojiButton}
showMessageSendButton={showMessageSendButton}
showVoiceMessageButton={false}
expressionPickerOpen={expressionPickerOpen}
selectedTab={selectedTab}
isMobile={mobileLayout.enabled}
isSlowmodeActive={isSubmissionBlockedBySlowmode}
isOverLimit={isOverCharacterLimit}
hasContent={hasMessageContent}
hasAttachments={uploadAttachments.length > 0}
expressionPickerTriggerRef={expressionPickerTriggerRef}
invisibleExpressionPickerTriggerRef={invisibleExpressionPickerTriggerRef}
onExpressionPickerToggle={handleExpressionPickerTabToggle}
onSubmit={handleSubmit}
channelId={channel.id}
</flx-channel-textarea-status-rail-left>
{isSlowmodeIndicatorVisible && (
<flx-channel-textarea-slowmode-slot className={flxElementClassName(wrapperStyles.statusSlowmodeSlot)}>
<SlowmodeIndicator
slowmodeRemaining={slowmodeRemaining}
slowmodeDuration={channel.rateLimitPerUser * 1000}
isImmune={isSlowmodeImmune}
/>
</flx-channel-textarea-box>,
styles.inputSection,
</flx-channel-textarea-slowmode-slot>
)}
<MessageCharacterCounter
currentLength={trimmedMessageContent.length}
maxLength={maxMessageLength}
canUpgrade={maxMessageLength < premiumMaxLength}
premiumMaxLength={premiumMaxLength}
/>
</flx-channel-textarea>
</FocusRing>
</flx-channel-textarea-status-rail>
{hasLeadingStatusContent && (
<div className={wrapperStyles.composerActionStack} data-flx="channel.textarea.composer-action-stack">
{topBarContent !== null && (
<div className={wrapperStyles.composerActionRow} data-flx="channel.textarea.composer-action-row">
{topBarContent}
</div>
)}
{slashCommandState.activeSlot !== null && (
<div className={wrapperStyles.composerActionRow} data-flx="channel.textarea.slash-command-action-row">
<SlashCommandParamBar activeSlot={slashCommandState.activeSlot} onClear={handleClearSlashCommand} />
</div>
)}
</div>
)}
{showAttachments &&
renderSection(<ChannelAttachmentArea channelId={channel.id} />, styles.collapsibleSection)}
{showStickers &&
renderSection(
<ChannelStickersArea channelId={channel.id} hasAttachments={hasAttachments} />,
styles.collapsibleSection,
)}
{renderSection(
<flx-channel-textarea-box
className={flxElementClassName(styles.mainWrapperDense, textareaInputDisabled && wrapperStyles.disabled)}
>
<flx-channel-textarea-upload-column
className={flxElementClassName(styles.uploadButtonColumn, styles.sideButtonPadding)}
>
<TextareaButton
iconProps={PLUS_ICON_PROPS}
icon={slashCommandState.hasSlots ? SlashCommandIcon : PlusIcon}
label={slashCommandState.hasSlots ? i18n._(CLEAR_COMMAND_DESCRIPTOR) : i18n._(OPEN_MENU_DESCRIPTOR)}
disabled={textareaInputDisabled}
aria-hidden={textareaInputDisabled ? true : undefined}
onMouseDown={slashCommandState.hasSlots ? undefined : handlePlusMenuMouseDown}
onClick={slashCommandState.hasSlots ? handleClearSlashCommand : handlePlusMenuClick}
forceHover={!slashCommandState.hasSlots && plusContextMenuOpen}
className={plusContextMenuOpen ? styles.plusButtonAboveBackdrop : undefined}
ref={plusButtonRef}
/>
</flx-channel-textarea-upload-column>
<flx-channel-textarea-content
ref={contentAreaRef}
className={flxElementClassName(styles.contentAreaDense)}
>
<flx-channel-textarea-composer className={flxElementClassName(lexicalStyles.composerHost)}>
<LexicalComposerInput
placeholder={placeholderText}
disabled={textareaInputDisabled}
handleRef={handleRef}
initialValue={initialDraftRef.current.display}
initialSegments={initialDraftRef.current.segments}
slotResolvers={slotResolvers}
emojiShortcodeResolver={composerEmojiResolver}
channelId={channel.id}
guildId={channel.guildId}
submitOnEnter={!mobileLayout.enabled}
focusRingTarget={containerRef}
focusRingEnabled={!textareaInputDisabled && Accessibility.showTextareaFocusRing}
className={lexicalStyles.composerEditable}
autocompleteOptions={autocompleteOptions}
autocompleteType={autocompleteType}
autocompleteQuery={autocompleteQuery}
autocompleteEnabled={!textareaInputDisabled}
slotMenuActive={isSlotMenu}
autocompleteReferenceElement={containerRef.current}
autocompleteListboxId={autocompleteListId}
onAutocompleteSelect={handleSelect}
onChange={handleEditorChange}
onCursorMove={onCursorMove}
onEnter={handleSubmit}
onArrowUp={handleArrowUpEmpty}
onKeyDown={handleEditorKeyDown}
onFocus={() => {
setIsFocused(true);
setIsInputAreaFocused(true);
ChannelSearch.setInputFocused(channel.id, false);
}}
onBlur={() => {
setIsFocused(false);
setIsInputAreaFocused(false);
}}
onSlashCommandStateChange={setSlashCommandState}
/>
</flx-channel-textarea-composer>
</flx-channel-textarea-content>
<TextareaButtons
disabled={textareaInputDisabled}
showAllButtons={showAllButtons}
showGifButton={showGifButton}
showMemesButton={showMemesButton}
showStickersButton={showStickersButton}
showEmojiButton={showEmojiButton}
showMessageSendButton={showMessageSendButton}
showVoiceMessageButton={false}
expressionPickerOpen={expressionPickerOpen}
selectedTab={selectedTab}
isMobile={mobileLayout.enabled}
isSlowmodeActive={isSubmissionBlockedBySlowmode}
isOverLimit={isOverCharacterLimit}
hasContent={hasMessageContent}
hasAttachments={uploadAttachments.length > 0}
expressionPickerTriggerRef={expressionPickerTriggerRef}
invisibleExpressionPickerTriggerRef={invisibleExpressionPickerTriggerRef}
onExpressionPickerToggle={handleExpressionPickerTabToggle}
onSubmit={handleSubmit}
channelId={channel.id}
/>
</flx-channel-textarea-box>,
styles.inputSection,
)}
<MessageCharacterCounter
currentLength={trimmedMessageContent.length}
maxLength={maxMessageLength}
canUpgrade={maxMessageLength < premiumMaxLength}
premiumMaxLength={premiumMaxLength}
/>
</flx-channel-textarea>
{mobileLayout.enabled && (
<>
<ExpressionPickerSheet
@@ -2,6 +2,7 @@
.container {
position: absolute;
right: 0.5rem;
bottom: 0.5rem;
right: var(--composer-box-padding-inline, 0px);
bottom: calc(100% + var(--composer-status-safe-gap, 0.5rem));
z-index: 4;
}
@@ -4,9 +4,11 @@
display: flex;
align-items: center;
gap: 0.5rem;
min-height: var(--composer-status-pill-height, 1.5rem);
min-height: 2.25rem;
min-width: 0;
padding: 0 0.25rem 0 0.625rem;
width: 100%;
padding: 0 0.375rem 0 0.75rem;
box-sizing: border-box;
}
.text {
@@ -15,7 +17,7 @@
gap: 0.4375rem;
overflow: hidden;
min-width: 0;
flex: 0 1 auto;
flex: 1 1 auto;
}
.name {
@@ -46,6 +48,7 @@
display: flex;
align-items: center;
flex: 0 0 auto;
margin-left: auto;
}
.button {
@@ -56,17 +59,15 @@
flex-shrink: 0;
border: none;
background-color: transparent;
width: var(--composer-status-pill-height, 1.5rem);
height: var(--composer-status-pill-height, 1.5rem);
width: var(--composer-action-button-size, 2rem);
height: var(--composer-action-button-size, 2rem);
padding: 0;
border-radius: var(--radius-full);
color: var(--text-primary-muted);
line-height: 0;
}
.button:hover {
color: var(--text-primary);
background-color: var(--background-modifier-hover);
}
.icon {
@@ -4,34 +4,17 @@
display: flex;
align-items: center;
gap: 0.1875rem;
height: var(--composer-status-pill-height, var(--slowmode-indicator-height));
height: var(--slowmode-indicator-height, 1.125rem);
max-width: 100%;
min-width: 0;
padding: 0 0.5rem;
border-radius: var(--radius-full);
background-color: var(--background-tertiary);
border: 0.0625rem solid color-mix(in srgb, var(--background-modifier-accent) 80%, transparent);
color: var(--text-primary-muted);
font-size: 0.75rem;
line-height: 1;
transition:
color 150ms ease,
background-color 150ms ease,
border-color 150ms ease;
transition: color 150ms ease;
}
.cooldown {
color: var(--status-danger);
background-color: color-mix(in srgb, var(--status-danger) 14%, transparent);
border-color: color-mix(in srgb, var(--status-danger) 45%, transparent);
}
.time {
flex: 0 0 auto;
font-weight: 500;
font-family: monospace;
font-size: 0.75rem;
line-height: 1;
}
.label {
@@ -42,4 +25,9 @@
font-weight: 600;
font-size: 0.75rem;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.icon {
flex: 0 0 auto;
}
@@ -16,22 +16,29 @@ import {ClockIcon} from '@phosphor-icons/react';
import {clsx} from 'clsx';
import {observer} from 'mobx-react-lite';
const SLOWMODE_IS_ENABLED_BUT_YOU_ARE_IMMUNE_DESCRIPTOR = msg({
message: 'Slowmode is enabled, but you are immune.',
comment: 'Description text in the channel and chat slowmode indicator.',
});
const YOU_ARE_IN_SLOWMODE_PLEASE_WAIT_BEFORE_SENDING_DESCRIPTOR = msg({
message: "You're in slowmode. Wait before sending another message.",
comment: 'Description text in the channel and chat slowmode indicator.',
});
const SLOWMODE_IS_ENABLED_FOR_THIS_CHANNEL_DESCRIPTOR = msg({
message: 'Slowmode is enabled for this channel.',
comment: 'Description text in the channel and chat slowmode indicator.',
});
const SLOWMODE_DESCRIPTOR = msg({
message: '{durationLabel} slowmode',
const SLOWMODE_IS_SET_BUT_YOU_ARE_IMMUNE_DESCRIPTOR = msg({
message: 'Slowmode is set to {durationLabel}, but you are immune.',
comment:
'Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.',
'Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.',
});
const SLOWMODE_IS_SET_WAIT_BEFORE_SENDING_DESCRIPTOR = msg({
message: 'Slowmode is set to {durationLabel}. Wait before sending another message.',
comment:
'Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.',
});
const SLOWMODE_IS_SET_FOR_THIS_CHANNEL_DESCRIPTOR = msg({
message: 'Slowmode is set to {durationLabel} for this channel.',
comment:
'Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.',
});
const SLOWMODE_IS_ENABLED_DESCRIPTOR = msg({
message: 'Slowmode is enabled',
comment: 'Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.',
});
const SLOWMODE_IS_ACTIVE_DESCRIPTOR = msg({
message: 'Slowmode is active ({remaining})',
comment:
'Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.',
});
interface SlowmodeIndicatorProps {
@@ -83,28 +90,36 @@ export const SlowmodeIndicator = observer(({slowmodeRemaining, slowmodeDuration,
const {i18n} = useLingui();
const locale = i18n.locale;
const onCooldown = !isImmune && slowmodeRemaining > 0;
const tooltipText = isImmune
? i18n._(SLOWMODE_IS_ENABLED_BUT_YOU_ARE_IMMUNE_DESCRIPTOR)
: onCooldown
? i18n._(YOU_ARE_IN_SLOWMODE_PLEASE_WAIT_BEFORE_SENDING_DESCRIPTOR)
: i18n._(SLOWMODE_IS_ENABLED_FOR_THIS_CHANNEL_DESCRIPTOR);
const durationLabel = formatSlowmodeDuration(slowmodeDuration, locale);
let tooltipText: string;
if (isImmune) {
tooltipText = i18n._(SLOWMODE_IS_SET_BUT_YOU_ARE_IMMUNE_DESCRIPTOR, {durationLabel});
} else if (onCooldown) {
tooltipText = i18n._(SLOWMODE_IS_SET_WAIT_BEFORE_SENDING_DESCRIPTOR, {durationLabel});
} else {
tooltipText = i18n._(SLOWMODE_IS_SET_FOR_THIS_CHANNEL_DESCRIPTOR, {durationLabel});
}
let statusLabel: string;
if (onCooldown) {
statusLabel = i18n._(SLOWMODE_IS_ACTIVE_DESCRIPTOR, {remaining: formatSlowmodeTime(slowmodeRemaining, locale)});
} else {
statusLabel = i18n._(SLOWMODE_IS_ENABLED_DESCRIPTOR);
}
return (
<Tooltip text={tooltipText} data-flx="channel.slowmode-indicator.tooltip">
<div
className={clsx(styles.container, onCooldown && styles.cooldown)}
data-flx="channel.slowmode-indicator.container"
>
<ClockIcon size={remFromPx(12)} weight="fill" data-flx="channel.slowmode-indicator.clock-icon" />
{onCooldown ? (
<span className={styles.time} data-flx="channel.slowmode-indicator.time">
{formatSlowmodeTime(slowmodeRemaining, locale)}
</span>
) : (
<span className={styles.label} data-flx="channel.slowmode-indicator.label">
{i18n._(SLOWMODE_DESCRIPTOR, {durationLabel})}
</span>
)}
<span className={styles.label} data-flx="channel.slowmode-indicator.label">
{statusLabel}
</span>
<ClockIcon
size={remFromPx(12)}
weight="fill"
className={styles.icon}
data-flx="channel.slowmode-indicator.clock-icon"
/>
</div>
</Tooltip>
);
@@ -5,9 +5,19 @@
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
color: var(--text-primary-muted);
}
.username {
font-weight: 600;
}
.composerStatus {
display: inline-flex;
align-items: center;
height: var(--typing-pill-height, 1.125rem);
max-width: 100%;
min-width: 0;
color: var(--text-primary-muted);
pointer-events: auto;
}
@@ -118,10 +118,7 @@ export const TypingUsers = observer(
className={`${messageStyles.typingContainer} ${messageStyles.typingCluster} ${messageStyles.typingClusterComposerStatus}`}
data-flx="channel.typing-users.div"
>
<div
className={`${messageStyles.typingPill} ${messageStyles.typingPillComposerStatus}`}
data-flx="channel.typing-users.div--2"
>
<div className={styles.composerStatus} data-flx="channel.typing-users.div--2">
<div className={messageStyles.typingIndicator} data-flx="channel.typing-users.div--3">
<Typing
className={styles.typing}
@@ -3,11 +3,11 @@
.barrierLayout {
display: grid;
grid-template-columns: minmax(0, auto) minmax(0, 1fr) minmax(0, auto);
align-items: flex-start;
align-items: start;
position: relative;
min-height: var(--textarea-min-height);
box-sizing: border-box;
padding: var(--user-area-padding-y) 0;
padding: var(--textarea-padding-y) 0;
column-gap: var(--textarea-upload-gap);
min-width: 0;
width: 100%;
@@ -51,12 +51,18 @@
.actionArea > button {
margin: 0;
height: var(--textarea-button-height);
min-height: var(--textarea-button-height);
}
.timer {
display: inline-flex;
align-items: center;
box-sizing: border-box;
height: var(--textarea-button-height);
border-radius: 0.375rem;
background-color: var(--background-secondary-alt);
padding: 0.375rem 0.75rem;
padding: 0 0.75rem;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.25rem;
@@ -55,9 +55,8 @@ const BarrierBase = observer(({message, action, icon}: BarrierBaseProps) => {
wrapperStyles.box,
wrapperStyles.wrapperSides,
textareaStyles.textareaOuter,
textareaStyles.textareaOuterMinHeight,
textareaStyles.textareaOuterRow,
wrapperStyles.roundedAll,
wrapperStyles.bottomSpacing,
)}
data-flx="channel.barriers.barrier-components.barrier-base.div"
>
@@ -5,13 +5,14 @@ import {SettingsControlRow} from '@app/features/channel/components/modals/channe
import type {FormInputs} from '@app/features/channel/components/modals/channel_tabs/channel_overview_tab/shared';
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
import {formatPermissionLabel} from '@app/features/permissions/utils/PermissionUtils';
import {RESET_SLIDER_TO_DEFAULT_VALUE_DESCRIPTOR, Slider} from '@app/features/ui/components/Slider';
import {Slider} from '@app/features/ui/components/Slider';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
import {CHANNEL_RATE_LIMIT_PER_USER_MAX, CHANNEL_RATE_LIMIT_PER_USER_MIN} from '@fluxer/constants/src/LimitConstants';
import {SECONDS_PER_HOUR, SECONDS_PER_MINUTE} from '@fluxer/date_utils/src/DateConstants';
import type {I18n} from '@lingui/core';
import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/macro';
import {formatListWithConfig} from '@pkgs/list_utils/src/ListFormatting';
import type React from 'react';
import {Controller, type UseFormReturn} from 'react-hook-form';
@@ -33,24 +34,33 @@ const SLOWMODE_DESCRIPTION_DESCRIPTOR = msg({
const SLOWMODE_STOP_SECONDS: ReadonlyArray<number> = [
0, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 7200, 21600,
];
const SLOWMODE_MARKER_SECONDS: ReadonlyArray<number> = [0, 60, 3600, 21600];
type SlowmodeDurationUnit = 'second' | 'minute' | 'hour';
function formatSlowmodeDurationPart(value: number, unit: SlowmodeDurationUnit, locale: string): string {
return getCachedNumberFormat(locale, {style: 'unit', unit, unitDisplay: 'long'}).format(value);
}
function formatSlowmodeDuration(i18n: I18n, seconds: number): string {
const roundedSeconds = Math.max(0, Math.round(seconds));
if (roundedSeconds === 0) {
return i18n._(OFF_DESCRIPTOR);
}
let unit: SlowmodeDurationUnit = 'second';
let value = roundedSeconds;
if (roundedSeconds % SECONDS_PER_HOUR === 0) {
unit = 'hour';
value = roundedSeconds / SECONDS_PER_HOUR;
} else if (roundedSeconds % SECONDS_PER_MINUTE === 0) {
unit = 'minute';
value = roundedSeconds / SECONDS_PER_MINUTE;
const hours = Math.floor(roundedSeconds / SECONDS_PER_HOUR);
const minutes = Math.floor((roundedSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE);
const remainingSeconds = roundedSeconds % SECONDS_PER_MINUTE;
const parts: Array<string> = [];
if (hours > 0) {
parts.push(formatSlowmodeDurationPart(hours, 'hour', i18n.locale));
}
return getCachedNumberFormat(i18n.locale, {style: 'unit', unit, unitDisplay: 'long'}).format(value);
if (minutes > 0) {
parts.push(formatSlowmodeDurationPart(minutes, 'minute', i18n.locale));
}
if (remainingSeconds > 0) {
parts.push(formatSlowmodeDurationPart(remainingSeconds, 'second', i18n.locale));
}
return formatListWithConfig(parts, {locale: i18n.locale, style: 'long', type: 'unit'});
}
interface SlowmodeControlProps {
@@ -61,7 +71,6 @@ export const SlowmodeControl: React.FC<SlowmodeControlProps> = ({form}) => {
const {i18n} = useLingui();
const slowmodeLabel = i18n._(SLOWMODE_DESCRIPTOR);
const bypassSlowmodePermissionLabel = formatPermissionLabel(i18n, Permissions.BYPASS_SLOWMODE);
const resetSliderLabel = i18n._(RESET_SLIDER_TO_DEFAULT_VALUE_DESCRIPTOR);
return (
<Controller
name="slowmode"
@@ -92,12 +101,14 @@ export const SlowmodeControl: React.FC<SlowmodeControlProps> = ({form}) => {
markers={SLOWMODE_STOP_SECONDS}
ariaLabel={slowmodeLabel}
ariaValueText={formatSlowmodeDuration(i18n, currentSeconds)}
onMarkerRender={(seconds) => formatSlowmodeDuration(i18n, seconds)}
onMarkerRender={(seconds) => {
if (!SLOWMODE_MARKER_SECONDS.includes(seconds)) {
return null;
}
return formatSlowmodeDuration(i18n, seconds);
}}
onValueRender={(seconds) => formatSlowmodeDuration(i18n, Math.round(seconds))}
onValueChange={(seconds) => field.onChange(Math.round(seconds))}
showResetButton={true}
onReset={() => field.onChange(0)}
resetTooltip={resetSliderLabel}
/>
</div>
</SettingsControlRow>
@@ -1,19 +1,12 @@
/* SPDX-License-Identifier: AGPL-3.0-or-later */
.wrapperSides {
margin-left: 0;
margin-right: 0;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.box {
position: relative;
background-color: var(--background-secondary-lighter);
border: none;
transition: border-color 0.2s ease;
margin-bottom: 0;
max-width: 100%;
box-sizing: border-box;
}
@@ -28,15 +21,11 @@
}
.stackSection + .stackSection {
border-top: 0.0625rem solid var(--user-area-divider-color);
}
.box:focus-within {
border-top: none;
border-top: 0.0625rem solid var(--composer-divider-color, var(--user-area-divider-color));
}
.roundedAll {
border-radius: 0;
border-radius: var(--composer-box-radius, 0px);
}
.roundedTop {
@@ -44,32 +33,42 @@
border-top-right-radius: 0;
}
.roundedBottom {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.noBottomBorder {
border-bottom-width: 0;
}
.bottomSpacing {
margin-bottom: 0;
}
.disabled {
opacity: 0.6;
cursor: not-allowed;
}
.statusRail {
--composer-status-pill-height: 1.5rem;
--typing-pill-height: var(--composer-status-pill-height);
--slowmode-indicator-height: var(--composer-status-pill-height);
.composerRoot {
--composer-action-button-size: 2rem;
--composer-divider-color: var(--floating-surface-ring-color-strong);
}
.composerRoot::before {
content: '';
position: absolute;
inset-inline: var(--composer-status-rail-inset, 0);
top: 0;
transform: translateY(-50%);
bottom: calc(100% + var(--composer-box-inset, 0px));
inset-inline: calc(-1 * var(--composer-box-inset-inline, 0px));
height: max(0px, calc(var(--composer-status-safe-area) - var(--composer-box-inset, 0px)));
background: linear-gradient(
to bottom,
transparent 0%,
color-mix(in srgb, var(--composer-surface-color) 42%, transparent) 34%,
color-mix(in srgb, var(--composer-surface-color) 82%, transparent) 66%,
var(--composer-surface-color) 100%
);
pointer-events: none;
}
.statusRail {
--typing-pill-height: var(--composer-status-line-height, 1.125rem);
--slowmode-indicator-height: var(--composer-status-line-height, 1.125rem);
position: absolute;
inset-inline: var(--composer-box-padding-inline, 0px);
bottom: calc(100% + var(--composer-status-safe-gap, 0.5rem));
z-index: 3;
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, auto);
@@ -77,24 +76,17 @@
gap: 0.5rem;
min-width: 0;
max-width: 100%;
min-height: calc(var(--composer-status-pill-height) + 0.5rem);
padding: 0.25rem 0;
height: var(--composer-status-line-height, 1.125rem);
box-sizing: border-box;
text-shadow:
0 0 0.1875rem var(--composer-surface-color),
0 0 0.5rem var(--composer-surface-color);
pointer-events: none;
}
.statusRailInFlow {
position: static;
inset: auto;
transform: none;
z-index: auto;
padding-top: 0.5rem;
}
.statusRailLeft {
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
max-width: 100%;
overflow: hidden;
@@ -109,11 +101,6 @@
pointer-events: auto;
}
.statusRailLeftWithLeading .statusTypingSlot {
margin-left: auto;
max-width: min(22rem, 45%);
}
.statusTypingSlot > * {
min-width: 0;
max-width: 100%;
@@ -126,17 +113,36 @@
pointer-events: auto;
}
.topBarContainer {
position: relative;
z-index: 1;
max-width: 100%;
min-width: 0;
box-sizing: border-box;
.composerActionStack {
display: flex;
justify-content: flex-start;
pointer-events: auto;
width: fit-content;
flex: 0 1 auto;
flex: 0 0 auto;
flex-direction: column;
margin-inline: calc(-1 * var(--composer-box-padding-inline, 0px));
min-width: 0;
width: auto;
overflow: hidden;
box-sizing: border-box;
border-bottom: 0.0625rem solid var(--composer-divider-color, var(--background-modifier-accent));
border-radius: var(--composer-box-radius, 0px) var(--composer-box-radius, 0px) 0 0;
background-color: color-mix(in srgb, var(--background-textarea) 84%, var(--background-tertiary) 16%);
}
.composerActionRow {
display: flex;
min-width: 0;
width: 100%;
max-width: 100%;
box-sizing: border-box;
}
.composerActionRow + .composerActionRow {
border-top: 0.0625rem solid color-mix(in srgb, var(--background-modifier-accent) 72%, transparent);
}
.composerActionRow > * {
width: 100%;
max-width: 100%;
background-color: transparent;
}
.barInner {
@@ -150,39 +156,16 @@
box-sizing: border-box;
}
.topBarContainer > * {
pointer-events: auto;
}
.topBarContainer > .box {
width: fit-content;
max-width: min(30rem, 100%);
border-radius: var(--radius-full);
background-color: color-mix(in srgb, var(--background-textarea) 84%, var(--background-tertiary) 16%);
border: 0.0625rem solid color-mix(in srgb, var(--background-modifier-accent) 82%, transparent);
overflow: hidden;
}
.topBarContainer .barInner {
display: flex;
align-items: center;
gap: 0.5rem;
min-height: var(--composer-status-pill-height);
padding: 0 0.25rem 0 0.625rem;
width: 100%;
min-width: 0;
}
.topBarContainer .barInner > :first-child {
min-width: 0;
flex: 1 1 auto;
}
.separator {
height: 0.0625rem;
background-color: transparent;
}
.topBarContainer .separator {
.composerActionRow .barInner {
min-height: 2.25rem;
padding-inline: 0.75rem 0.375rem;
}
.composerActionRow .separator {
display: none;
}
@@ -9,7 +9,10 @@
height: var(--textarea-button-height);
padding: 0;
color: var(--text-primary-muted);
transition: color var(--transition-normal);
border-radius: var(--radius-md);
transition:
color var(--transition-normal),
background-color var(--transition-normal);
cursor: pointer;
background: transparent;
border: none;
@@ -20,6 +23,7 @@
.button:hover:not(:disabled),
.button.contextMenuHover:not(:disabled) {
color: var(--text-primary);
background-color: var(--background-modifier-hover);
}
.button:disabled {
@@ -2,19 +2,17 @@
:root {
--textarea-font-size: var(--font-size, 1rem);
--textarea-button-height: var(--user-area-content-height);
--textarea-button-icon-size: 1.625rem;
--textarea-button-height: 2rem;
--textarea-button-icon-size: 1.375rem;
--textarea-button-padding-x: 0px;
--textarea-button-min-width: auto;
--textarea-button-compact-height: var(--user-area-content-height);
--textarea-button-compact-icon-size: 1.375rem;
--textarea-button-compact-height: 2rem;
--textarea-button-compact-icon-size: 1.25rem;
--textarea-container-padding-y: 0px;
--textarea-container-padding-x: 0px;
--textarea-min-height: var(--input-container-min-height);
--textarea-horizontal-padding: var(--chat-horizontal-padding, var(--spacing-4));
--textarea-content-offset: calc((var(--user-area-content-height) - var(--textarea-line-height)) / 2);
--textarea-content-offset: max(0rem, calc((var(--textarea-button-height) - var(--textarea-line-height)) / 2));
--textarea-upload-gap: var(--message-gutter, 1rem);
--textarea-side-button-padding: max(
0px,
@@ -92,7 +90,7 @@
}
.textarea::placeholder {
color: var(--text-primary-muted);
color: var(--text-tertiary-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -168,7 +166,7 @@
width: 0.0625rem;
height: 1.5rem;
background-color: var(--background-modifier-hover);
margin: 0 0.25rem;
margin-inline: var(--composer-action-gap);
align-self: center;
}
@@ -176,17 +174,12 @@
display: flex;
flex-direction: column;
flex: 0 1 auto;
min-height: 0;
padding-left: var(--textarea-horizontal-padding);
padding-right: var(--textarea-horizontal-padding);
padding-inline: var(--composer-box-padding-inline);
box-sizing: border-box;
box-shadow: inset 0 0.0625rem 0 var(--user-area-divider-color);
width: 100%;
max-width: 100%;
min-width: 0;
contain: inline-size;
overflow: visible;
--composer-status-rail-inset: var(--textarea-horizontal-padding);
}
.collapsibleSection {
@@ -201,7 +194,12 @@
.textareaOuterMobile {
padding: 0;
--composer-status-rail-inset: 0px;
min-height: 0;
--textarea-min-height: var(--composer-mobile-box-height);
--textarea-padding-y: var(--composer-mobile-padding-y);
--composer-box-inset: 0px;
--composer-box-inset-inline: 0px;
--composer-box-padding-inline: 0px;
}
.textareaMobile {
@@ -211,21 +209,17 @@
.mainWrapperDense {
display: grid;
grid-template-columns: minmax(0, auto) minmax(0, 1fr) minmax(0, auto);
align-items: flex-start;
align-items: start;
position: relative;
min-height: var(--textarea-min-height);
box-sizing: border-box;
padding: var(--user-area-padding-y) 0;
padding: var(--textarea-padding-y) 0;
column-gap: var(--textarea-upload-gap);
min-width: 0;
width: 100%;
max-width: 100%;
}
.textareaFocusRing {
border-radius: var(--radius-xl);
}
.mainWrapperEditing {
display: flex;
align-items: flex-start;
@@ -234,7 +228,7 @@
background-color: var(--background-textarea);
border: 0.0625rem solid var(--background-modifier-accent);
border-radius: var(--radius-md);
transition: colors;
transition: border-color 0.15s cubic-bezier(0.4, 0, 0.2, 1);
--scrollbar-track-bg: var(--background-textarea);
}
@@ -253,7 +247,7 @@
display: flex;
align-items: center;
justify-content: center;
min-height: var(--user-area-content-height);
min-height: var(--textarea-button-height);
min-width: 0;
flex-shrink: 1;
}
@@ -271,7 +265,7 @@
grid-column: 2;
display: flex;
flex-direction: column;
min-height: var(--user-area-content-height);
min-height: var(--textarea-button-height);
min-width: 0;
padding-top: var(--textarea-content-offset);
}
@@ -300,9 +294,9 @@
.buttonContainerDense {
grid-column: 3;
display: flex;
align-items: flex-start;
gap: 0.625rem;
min-height: var(--user-area-content-height);
align-items: center;
gap: var(--composer-action-gap);
min-height: var(--textarea-button-height);
min-width: 0;
flex-shrink: 1;
}
@@ -371,6 +365,26 @@
min-width: 0;
}
.textareaOuterMinHeight {
min-height: var(--input-container-min-height);
.textareaOuterRow {
min-height: var(--textarea-min-height);
margin-block: var(--composer-box-inset);
margin-inline: var(--composer-box-inset-inline);
background-color: var(--background-textarea);
--composer-box-radius: var(--footer-box-radius);
--composer-box-ring-color: var(--floating-surface-ring-color);
--scrollbar-track-bg: var(--background-textarea);
}
.textareaOuterRow::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: inset 0 0 0 0.0625rem var(--composer-box-ring-color);
transition: box-shadow 0.15s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
}
.textareaOuterRow:focus-within {
--composer-box-ring-color: var(--floating-surface-ring-color-strong);
}
@@ -1,12 +1,9 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {requestChannelComposerAffordanceDismissal} from '@app/features/channel/components/ChannelComposerDismissal';
import type {Channel} from '@app/features/channel/models/Channel';
import type {ComposerHandle} from '@app/features/lexical/composer/ComposerHandle';
import * as MessageCommands from '@app/features/messaging/commands/MessageCommands';
import type {Message} from '@app/features/messaging/models/MessagingMessage';
import MessageEdit from '@app/features/messaging/state/MessageEdit';
import MessageFocus from '@app/features/messaging/state/MessageFocus';
import type {MessageReplyState} from '@app/features/messaging/state/MessageReply';
import {ComponentDispatch} from '@app/features/platform/utils/ComponentBus';
import {canFocusTextarea, safeFocus} from '@app/features/platform/utils/InputFocusManager';
import {isTextInputKeyEvent} from '@app/features/platform/utils/IsTextInputKeyEvent';
@@ -23,15 +20,6 @@ interface UseChannelComposerGlobalShortcutsParams {
textareaInputDisabled: boolean;
isFocused: boolean;
handleArrowUpEmpty: () => void;
editingMessage: Message | null | undefined;
replyingMessage: MessageReplyState | null;
mobileLayout: ComposerMobileLayout;
setValue: React.Dispatch<React.SetStateAction<string>>;
clearSegments: () => void;
}
interface ComposerMobileLayout {
enabled: boolean;
}
export function useChannelComposerGlobalShortcuts({
@@ -41,11 +29,6 @@ export function useChannelComposerGlobalShortcuts({
textareaInputDisabled,
isFocused,
handleArrowUpEmpty,
editingMessage,
replyingMessage,
mobileLayout,
setValue,
clearSegments,
}: UseChannelComposerGlobalShortcutsParams): void {
useEffect(() => {
if (textareaInputDisabled) {
@@ -108,31 +91,19 @@ export function useChannelComposerGlobalShortcuts({
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'Escape') return;
if (event.shiftKey) return;
ComponentDispatch.dispatch('ESCAPE_PRESSED', {channelId: channel.id});
const isEditingInline = MessageEdit.getEditingMessageId(channel.id) != null;
if (isEditingInline) {
if (event.defaultPrevented || event.isComposing) return;
if (event.key !== 'Escape' || event.shiftKey) return;
if (requestChannelComposerAffordanceDismissal(channel.id)) {
event.preventDefault();
event.stopPropagation();
MessageCommands.stopEdit(channel.id);
return;
}
if (editingMessage && mobileLayout.enabled) {
event.preventDefault();
MessageCommands.stopEditMobile(channel.id);
setValue('');
clearSegments();
} else if (replyingMessage) {
event.preventDefault();
MessageCommands.stopReply(channel.id);
} else {
event.preventDefault();
}
ComponentDispatch.dispatch('ESCAPE_PRESSED', {channelId: channel.id});
event.preventDefault();
};
window.addEventListener('keydown', handleKeyDown);
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [channel.id, editingMessage, replyingMessage, mobileLayout.enabled, textareaInputDisabled, clearSegments]);
}, [channel.id, textareaInputDisabled]);
}
@@ -179,7 +179,7 @@
align-items: center;
justify-content: space-between;
width: 100%;
min-height: var(--input-container-min-height);
min-height: var(--list-row-min-height);
padding: 0 var(--input-container-padding);
background: transparent;
border: none;
@@ -355,7 +355,7 @@ msgstr "{count, plural, one {# إشارة مرجعية} few {# إشارات مر
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# مجتمع تم العثور عليه} other {# مجتمعات تم العثور عليها}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# مجموعة غير متاحة مؤقتًا بسبب عطل في مكثف التدفق.} other {# مجموعات غير متاحة مؤقتًا بسبب عطل في مكثف التدفق.}}"
@@ -378,8 +378,6 @@ msgstr "{count, plural, one {# إيموجي} few {# إيموجي} many {# إيم
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# هدية} other {# هدايا}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# ساعة} few {# ساعات} many {# ساعة} other {# ساعات}}"
@@ -397,11 +395,6 @@ msgstr "{count, plural, one {# عنصر} other {# عناصر}}"
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# عضو} other {# أعضاء}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# دقيقة} other {# دقائق}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# مجتمع مشترك} other {# مجتمعات مشتركة}}"
@@ -433,9 +426,7 @@ msgstr "{count, plural, one {# متفاعل} few {# متفاعلون} many {# م
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# وسائط محفوظة} other {# وسائط محفوظة}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# ثانية} other {# ثوانٍ}}"
@@ -667,11 +658,6 @@ msgstr "أمامك بـ {duration}"
msgid "{duration} behind you"
msgstr "متأخر عنك بـ {duration}"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "الوضع البطيء {durationLabel}"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2847,7 +2833,7 @@ msgstr "أوقف المسؤولون الدعوات مؤقتًا — لا يمك
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6546,8 +6532,8 @@ msgstr "نوع القناة: {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "تم تحديث القناة"
@@ -7162,7 +7148,7 @@ msgstr "مسح مرفقات الاختبار"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "مسح الأمر"
@@ -13509,7 +13495,7 @@ msgstr "أدخل رمز الرسالة النصية القصيرة"
msgid "Enter this code in your browser to complete sign-in."
msgstr "أدخل هذا الرمز في متصفحك لإكمال تسجيل الدخول."
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "اضغط Enter لـ <0><1>حفظ</1></0>"
@@ -13650,7 +13636,7 @@ msgstr "مفتاح الهروب"
msgid "Escape key exits keyboard mode"
msgstr "مفتاح الهروب يخرج من وضع لوحة المفاتيح"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "اضغط على Escape لـ <0><1>الإلغاء</1></0>"
@@ -22465,7 +22451,7 @@ msgstr "أشياء"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26668,7 +26654,7 @@ msgid "Reset premium state"
msgstr "إعادة تعيين حالة الاشتراك المميز"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "إعادة تعيين شريط التمرير إلى القيمة الافتراضية"
@@ -29896,7 +29882,7 @@ msgid "slash {commandName}"
msgstr "الأمر {commandName}/"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "قيمة شريط التمرير"
@@ -29968,7 +29954,7 @@ msgid "Slow profile load"
msgstr "تحميل بطيء للملف الشخصي"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "الوضع البطيء"
@@ -29984,21 +29970,36 @@ msgstr "الوضع البطيء · انتظر {remaining}"
msgid "Slowmode active"
msgstr "الوضع البطيء نشط"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "تم تفعيل الوضع البطيء لهذه القناة."
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "الوضع البطيء نشط ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "الوضع البطيء مفعّل، لكنك معفى."
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "الوضع البطيء مُفعّل"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "الوضع البطيء قيد التشغيل — انتظر {duration} قبل إرسال رسالة أخرى."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "الوضع البطيء مضبوط على {durationLabel} لهذه القناة."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "الوضع البطيء مضبوط على {durationLabel}، لكنك مُستثنى."
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "الوضع البطيء مضبوط على {durationLabel}. انتظر قبل إرسال رسالة أخرى."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36912,7 +36913,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "انتظر لحظة. ستُغلَق هذه النافذة تلقائيًا بمجرد استلام رسالتك."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "الانتظار بين الرسائل. يمكن لـ \"{bypassSlowmodePermissionLabel}\" تجاوز ذلك."
@@ -38500,11 +38501,6 @@ msgstr "أهلاً بك، {username}! سعيدون بانضمامك إلينا."
msgid "You're in preview mode"
msgstr "أنت في وضع المعاينة"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "أنت في وضع الحركة البطيئة. انتظر قبل إرسال رسالة أخرى."
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
File diff suppressed because it is too large. Load diff
@@ -355,7 +355,7 @@ msgstr "{count, plural, one {# отметка} other {# отметки}}"
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# общност намерена} other {# общности намерени}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# общност е временно недостъпна поради неизправност на флукс кондензатора.} other {# общности са временно недостъпни поради неизправност на флукс кондензатора.}}"
@@ -378,8 +378,6 @@ msgstr "{count, plural, one {# емоджи} other {# емоджита}}"
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# подарък} other {# подаръка}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# час} other {# часа}}"
@@ -397,11 +395,6 @@ msgstr "{count, plural, one {# елемент} other {# елемента}}"
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# член} other {# члена}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# минута} other {# минути}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# обща общност} other {# общи общности}}"
@@ -433,9 +426,7 @@ msgstr "{count, plural, one {# реакция} other {# реакции}}"
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# запазено мултимедийно съдържание} other {# запазени мултимедийни елемента}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# секунда} other {# секунди}}"
@@ -667,11 +658,6 @@ msgstr "{duration} пред вас"
msgid "{duration} behind you"
msgstr "с {duration} след вас"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "Забавен режим: {durationLabel}"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2847,7 +2833,7 @@ msgstr "Администраторите са поставили на пауза
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6546,8 +6532,8 @@ msgstr "Тип канал: {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "Каналът е актуализиран"
@@ -7162,7 +7148,7 @@ msgstr "Изчистване на прикачени макети"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "Изчисти команда"
@@ -13509,7 +13495,7 @@ msgstr "Въведете SMS кода"
msgid "Enter this code in your browser to complete sign-in."
msgstr "Въведете този код в браузъра си, за да завършите влизането."
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "натиснете Enter, за да <0><1>запазите</1></0>"
@@ -13650,7 +13636,7 @@ msgstr "клавиш Esc"
msgid "Escape key exits keyboard mode"
msgstr "Клавишът Esc излиза от режима на клавиатурата"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "натиснете Esc, за да <0><1>отмените</1></0>"
@@ -22465,7 +22451,7 @@ msgstr "Обекти"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26668,7 +26654,7 @@ msgid "Reset premium state"
msgstr "Нулиране на премиум статус"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "Възстановяване на плъзгача до стойността по подразбиране"
@@ -29896,7 +29882,7 @@ msgid "slash {commandName}"
msgstr "наклонена черта {commandName}"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "Стойност на плъзгача"
@@ -29968,7 +29954,7 @@ msgid "Slow profile load"
msgstr "Бавно зареждане на профил"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "Бавен режим"
@@ -29984,21 +29970,36 @@ msgstr "Забавен режим · изчакайте {remaining}"
msgid "Slowmode active"
msgstr "Забавен режим е активен"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "Бавен режим е активиран за този канал."
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "Бавен режим е активен ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "Забавеният режим е включен, но вие сте имунизирани."
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "Бавен режим е включен"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "Включен е бавен режим — изчакайте {duration}, преди да изпратите друго съобщение."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "Бавен режим е зададен на {durationLabel} за този канал."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "Бавен режим е зададен на {durationLabel}, но вие сте освободен."
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "Бавен режим е зададен на {durationLabel}. Изчакайте, преди да изпратите друго съобщение."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36912,7 +36913,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "Изчакайте малко. Този прозорец ще се затвори автоматично, щом получим съобщението ви."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "Време между съобщенията. „{bypassSlowmodePermissionLabel}“ може да го заобиколи."
@@ -38500,11 +38501,6 @@ msgstr "Вече си тук, {username}! Радваме се, че си с на
msgid "You're in preview mode"
msgstr "В режим на предварителен преглед сте"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "Включили сте бавен режим. Изчакайте, преди да изпратите друго съобщение."
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
@@ -355,7 +355,7 @@ msgstr "{count, plural, one {# záložka} few {# záložky} many {# záložek} o
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# komunita nalezena} other {# komunit nalezeno} few {# komunity nalezeny}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# komunita je dočasně nedostupná kvůli poruše \"flux kondenzátoru\".} few {# komunity jsou dočasně nedostupné kvůli poruše \"flux kondenzátoru\".} many {# komunit je dočasně nedostupných kvůli poruše \"flux kondenzátoru\".} other {# komunit je dočasně nedostupných kvůli poruše \"flux kondenzátoru\".}}"
@@ -378,8 +378,6 @@ msgstr "{count, plural, one {# emoji} few {# emoji} many {# emoji} other {# emoj
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# dárek} other {# dárky}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# hodina} few {# hodiny} many {# hodiny} other {# hodin}}"
@@ -397,11 +395,6 @@ msgstr "{count, plural, one {# položka} few {# položky} many {# položek} othe
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# člen} few {# členové} many {# členů} other {# členů}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# minuta} other {# minut}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# společná komunita} other {# společné komunity}}"
@@ -433,9 +426,7 @@ msgstr "{count, plural, one {# reakce} few {# reakce} many {# reakcí} other {#
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# uložená položka médií} few {# uložené položky médií} many {# uložených položek médií} other {# uložených položek médií}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# sekunda} few {# sekundy} many {# sekund} other {# sekund}}"
@@ -667,11 +658,6 @@ msgstr "O {duration} před vámi"
msgid "{duration} behind you"
msgstr "{duration} za vámi"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "Pomalý režim: {durationLabel}"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2847,7 +2833,7 @@ msgstr "Správci pozastavili pozvánky – momentálně se nemůžete připojit.
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6546,8 +6532,8 @@ msgstr "Typ kanálu: {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "Kanál aktualizován"
@@ -7162,7 +7148,7 @@ msgstr "Vymazat makety příloh"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "Vymazat příkaz"
@@ -13509,7 +13495,7 @@ msgstr "Zadejte kód SMS"
msgid "Enter this code in your browser to complete sign-in."
msgstr "Zadejte tento kód do prohlížeče a dokončete přihlášení."
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "enter pro <0><1>uložení</1></0>"
@@ -13650,7 +13636,7 @@ msgstr "Klávesa Escape"
msgid "Escape key exits keyboard mode"
msgstr "Klávesa Escape ukončí režim klávesnice"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "stiskněte Esc pro <0><1>zrušení</1></0>"
@@ -22465,7 +22451,7 @@ msgstr "Předměty"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26668,7 +26654,7 @@ msgid "Reset premium state"
msgstr "Resetovat stav prémiového účtu"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "Obnovit výchozí hodnotu posuvníku"
@@ -29896,7 +29882,7 @@ msgid "slash {commandName}"
msgstr "lomítko {commandName}"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "Hodnota posuvníku"
@@ -29968,7 +29954,7 @@ msgid "Slow profile load"
msgstr "Pomalé načítání profilu"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "Pomalý režim"
@@ -29984,21 +29970,36 @@ msgstr "Pomalý režim · čekejte {remaining}"
msgid "Slowmode active"
msgstr "Pomalý režim aktivní"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "V tomto kanálu je zapnutý pomalý režim."
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "Pomalý režim aktivní ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "Pomalý režim je zapnutý, ale jste imunní."
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "Pomalý režim je zapnutý"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "Pomalý režim je zapnutý – před odesláním další zprávy počkejte {duration}."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "Pomalý režim je v tomto kanálu nastaven na {durationLabel}."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "Pomalý režim je nastaven na {durationLabel}, ale vy jste vyjmuti."
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "Pomalý režim je nastaven na {durationLabel}. Před odesláním další zprávy počkejte."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36912,7 +36913,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "Moment. Toto okno se automaticky zavře, jakmile obdržíme vaši zprávu."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "Čas mezi zprávami. „{bypassSlowmodePermissionLabel}“ jej může obejít."
@@ -38500,11 +38501,6 @@ msgstr "Ahoj, {username}! Jsme rádi, že jsi s námi."
msgid "You're in preview mode"
msgstr "Jste v režimu náhledu"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "Jste v pomalém režimu. Před odesláním další zprávy počkejte."
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
@@ -355,7 +355,7 @@ msgstr "{count, plural, one {# bogmærke} other {# bogmærker}}"
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# fællesskab fundet} other {# fællesskaber fundet}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# fællesskab er midlertidigt utilgængeligt på grund af en fejl i fluxkondensatoren.} other {# fællesskaber er midlertidigt utilgængelige på grund af en fejl i fluxkondensatoren.}}"
@@ -378,8 +378,6 @@ msgstr "{count, plural, one {# emoji} other {# emojis}}"
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# gave} other {# gaver}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# time} other {# timer}}"
@@ -397,11 +395,6 @@ msgstr "{count, plural, one {# element} other {# elementer}}"
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# medlem} other {# medlemmer}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# minut} other {# minutter}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# fælles fællesskab} other {# fælles fællesskaber}}"
@@ -433,9 +426,7 @@ msgstr "{count, plural, one {# reaktion} other {# reaktioner}}"
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# gemt medieelement} other {# gemte medieelementer}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# sekund} other {# sekunder}}"
@@ -667,11 +658,6 @@ msgstr "{duration} foran dig"
msgid "{duration} behind you"
msgstr "{duration} efter dig"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "{durationLabel} slow-tilstand"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2847,7 +2833,7 @@ msgstr "Administratorer har sat invitationer på pause – du kan ikke deltage l
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6546,8 +6532,8 @@ msgstr "Kanaltype: {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "Kanal opdateret"
@@ -7162,7 +7148,7 @@ msgstr "Ryd vedhæftede filer (test)"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "Ryd kommando"
@@ -13509,7 +13495,7 @@ msgstr "Indtast sms-koden"
msgid "Enter this code in your browser to complete sign-in."
msgstr "Indtast denne kode i din browser for at logge ind."
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "tryk Enter for at <0><1>gemme</1></0>"
@@ -13650,7 +13636,7 @@ msgstr "Escape-tast"
msgid "Escape key exits keyboard mode"
msgstr "Escape-tasten afslutter tastaturtilstand"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "tryk på escape for at <0><1>annullere</1></0>"
@@ -22465,7 +22451,7 @@ msgstr "Objekter"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26668,7 +26654,7 @@ msgid "Reset premium state"
msgstr "Nulstil premium-status"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "Nulstil skyder til standardværdi"
@@ -29896,7 +29882,7 @@ msgid "slash {commandName}"
msgstr "slash {commandName}"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "Skyderens værdi"
@@ -29968,7 +29954,7 @@ msgid "Slow profile load"
msgstr "Langsom indlæsning af profil"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "Langsom tilstand"
@@ -29984,21 +29970,36 @@ msgstr "Langsom tilstand · vent {remaining}"
msgid "Slowmode active"
msgstr "Langsom tilstand aktiv"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "Slowmode er slået til for denne kanal."
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "Langsom tilstand er aktiv ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "Slowmode er slået til, men du er immun."
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "Langsom tilstand er aktiveret"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "Slowmode er slået til – vent {duration}, før du sender en ny besked."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "Langsom tilstand er sat til {durationLabel} for denne kanal."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "Langsom tilstand er sat til {durationLabel}, men du er fritaget."
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "Langsom tilstand er sat til {durationLabel}. Vent, før du sender en ny besked."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36912,7 +36913,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "Vent et øjeblik. Dette vindue lukkes automatisk, når vi modtager din besked."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "Ventetid mellem beskeder. \"{bypassSlowmodePermissionLabel}\" kan tilsidesætte dette."
@@ -38500,11 +38501,6 @@ msgstr "Velkommen, {username}! Godt at have dig med."
msgid "You're in preview mode"
msgstr "Du er i forhåndsvisningstilstand"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "Du er i slowmode. Vent, før du sender en ny besked."
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
@@ -355,7 +355,7 @@ msgstr "{count, plural, one {# Lesezeichen} other {# Lesezeichen}}"
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# Community gefunden} other {# Communitys gefunden}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# Community ist aufgrund einer Fehlfunktion des Fluxkompensators vorübergehend nicht verfügbar.} other {# Communitys sind aufgrund einer Fehlfunktion des Fluxkompensators vorübergehend nicht verfügbar.}}"
@@ -378,8 +378,6 @@ msgstr "{count, plural, one {# Emoji} other {# Emojis}}"
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# Geschenk} other {# Geschenke}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# Stunde} other {# Stunden}}"
@@ -397,11 +395,6 @@ msgstr "{count, plural, one {# Element} other {# Elemente}}"
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# Mitglied} other {# Mitglieder}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# Minute} other {# Minuten}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# gemeinsame Community} other {# gemeinsame Communitys}}"
@@ -433,9 +426,7 @@ msgstr "{count, plural, one {# Reaktion} other {# Reaktionen}}"
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# gespeichertes Medium} other {# gespeicherte Medien}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# Sekunde} other {# Sekunden}}"
@@ -667,11 +658,6 @@ msgstr "{duration} vor dir"
msgid "{duration} behind you"
msgstr "{duration} hinter dir"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "{durationLabel} langsamer Modus"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2847,7 +2833,7 @@ msgstr "Admins haben Einladungen pausiert – du kannst jetzt nicht beitreten."
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6546,8 +6532,8 @@ msgstr "Kanaltyp: {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "Kanal aktualisiert"
@@ -7162,7 +7148,7 @@ msgstr "Anhang-Mocks löschen"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "Befehl löschen"
@@ -13509,7 +13495,7 @@ msgstr "SMS-Code eingeben"
msgid "Enter this code in your browser to complete sign-in."
msgstr "Gib diesen Code in deinem Browser ein, um die Anmeldung abzuschließen."
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "Eingabetaste zum <0><1>Speichern</1></0>"
@@ -13650,7 +13636,7 @@ msgstr "Escape-Taste"
msgid "Escape key exits keyboard mode"
msgstr "Escape-Taste beendet den Tastaturmodus"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "Esc zum <0><1>Abbrechen</1></0>"
@@ -22465,7 +22451,7 @@ msgstr "Objekte"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26668,7 +26654,7 @@ msgid "Reset premium state"
msgstr "Premium-Status zurücksetzen"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "Schieberegler auf Standardwert zurücksetzen"
@@ -29896,7 +29882,7 @@ msgid "slash {commandName}"
msgstr "Slash {commandName}"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "Schiebereglerwert"
@@ -29968,7 +29954,7 @@ msgid "Slow profile load"
msgstr "Langsames Laden des Profils"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "Slow-Modus"
@@ -29984,21 +29970,36 @@ msgstr "Slow-Modus · warte {remaining}"
msgid "Slowmode active"
msgstr "Langsamer Modus aktiv"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "Der Langsam-Modus ist für diesen Kanal aktiviert."
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "Slow-Modus ist aktiv ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "Der Slow-Modus ist aktiviert, aber du bist ausgenommen."
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "Slow-Modus ist aktiviert"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "Der Langsam-Modus ist aktiv – warte {duration}, bevor du eine weitere Nachricht sendest."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "Der Slow-Modus ist für diesen Kanal auf {durationLabel} eingestellt."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "Der Slow-Modus ist auf {durationLabel} eingestellt, aber du bist davon ausgenommen."
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "Der Slow-Modus ist auf {durationLabel} eingestellt. Warte, bevor du eine weitere Nachricht sendest."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36912,7 +36913,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "Bitte warte einen Moment. Dieses Fenster schließt sich automatisch, sobald wir deine Nachricht erhalten."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "Wartezeit zwischen Nachrichten. \"{bypassSlowmodePermissionLabel}\" kann diese umgehen."
@@ -38500,11 +38501,6 @@ msgstr "Du bist da, {username}! Schön, dich bei uns zu haben."
msgid "You're in preview mode"
msgstr "Du bist im Vorschaumodus"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "Du bist im Slowmode. Warte, bevor du eine weitere Nachricht sendest."
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
@@ -355,7 +355,7 @@ msgstr "{count, plural, one {# σελιδοδείκτη} other {# σελιδοδ
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# κοινότητα βρέθηκε} other {# κοινότητες βρέθηκαν}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# κοινότητα είναι προσωρινά μη διαθέσιμη λόγω δυσλειτουργίας του πυκνωτή ροής.} other {# κοινότητες είναι προσωρινά μη διαθέσιμες λόγω δυσλειτουργίας του πυκνωτή ροής.}}"
@@ -378,8 +378,6 @@ msgstr "{count, plural, one {# emoji} other {# emoji}}"
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# δώρο} other {# δώρα}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# ώρα} other {# ώρες}}"
@@ -397,11 +395,6 @@ msgstr "{count, plural, one {# αντικείμενο} other {# αντικείμ
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# μέλος} other {# μέλη}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# λεπτό} other {# λεπτά}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# κοινή κοινότητα} other {# κοινές κοινότητες}}"
@@ -433,9 +426,7 @@ msgstr "{count, plural, one {# αντίδραση} other {# αντιδράσει
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# αποθηκευμένο πολυμέσο} other {# αποθηκευμένα πολυμέσα}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# δευτερόλεπτο} other {# δευτερόλεπτα}}"
@@ -667,11 +658,6 @@ msgstr "{duration} μπροστά από εσάς"
msgid "{duration} behind you"
msgstr "{duration} πίσω σας"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "Αργή λειτουργία {durationLabel}"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2847,7 +2833,7 @@ msgstr "Οι διαχειριστές έχουν θέσει σε παύση τι
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6546,8 +6532,8 @@ msgstr "Τύπος καναλιού: {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "Το κανάλι ενημερώθηκε"
@@ -7162,7 +7148,7 @@ msgstr "Εκκαθάριση εικονικών συνημμένων"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "Εκκαθάριση εντολής"
@@ -13509,7 +13495,7 @@ msgstr "Εισαγάγετε τον κωδικό SMS"
msgid "Enter this code in your browser to complete sign-in."
msgstr "Πληκτρολογήστε αυτόν τον κωδικό στο πρόγραμμα περιήγησής σας για να ολοκληρώσετε τη σύνδεση."
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "πατήστε enter για <0><1>αποθήκευση</1></0>"
@@ -13650,7 +13636,7 @@ msgstr "Πλήκτρο Esc"
msgid "Escape key exits keyboard mode"
msgstr "Το πλήκτρο Esc βγαίνει από τη λειτουργία πληκτρολογίου"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "πατήστε escape για <0><1>ακύρωση</1></0>"
@@ -22465,7 +22451,7 @@ msgstr "Αντικείμενα"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26668,7 +26654,7 @@ msgid "Reset premium state"
msgstr "Επαναφορά κατάστασης premium"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "Επαναφορά ρυθμιστικού στην προεπιλεγμένη τιμή"
@@ -29896,7 +29882,7 @@ msgid "slash {commandName}"
msgstr "/{commandName}"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "Τιμή ρυθμιστικού"
@@ -29968,7 +29954,7 @@ msgid "Slow profile load"
msgstr "Αργή φόρτωση προφίλ"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "Αργή λειτουργία"
@@ -29984,21 +29970,36 @@ msgstr "Αργή λειτουργία · αναμονή {remaining}"
msgid "Slowmode active"
msgstr "Ενεργή αργή λειτουργία"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "Η αργή λειτουργία είναι ενεργοποιημένη για αυτό το κανάλι."
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "Η αργή λειτουργία είναι ενεργή ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "Η αργή λειτουργία είναι ενεργοποιημένη, αλλά έχετε εξαίρεση."
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "Η αργή λειτουργία είναι ενεργοποιημένη"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "Η αργή λειτουργία είναι ενεργοποιημένη — περιμένετε {duration} πριν στείλετε άλλο μήνυμα."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "Η αργή λειτουργία έχει οριστεί σε {durationLabel} για αυτό το κανάλι."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "Η αργή λειτουργία έχει οριστεί σε {durationLabel}, αλλά εσείς είστε εξαιρούμενος."
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "Η αργή λειτουργία έχει οριστεί σε {durationLabel}. Περιμένετε πριν στείλετε άλλο μήνυμα."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36912,7 +36913,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "Περιμένετε λίγο. Αυτό το παράθυρο θα κλείσει αυτόματα μόλις λάβουμε το μήνυμά σας."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "Χρονικό διάστημα μεταξύ των μηνυμάτων. Η άδεια \"{bypassSlowmodePermissionLabel}\" μπορεί να το παρακάμψει."
@@ -38500,11 +38501,6 @@ msgstr "Είσαι εδώ, {username}! Χαιρόμαστε που σε έχου
msgid "You're in preview mode"
msgstr "Είστε σε λειτουργία προεπισκόπησης"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "Είστε σε λειτουργία αργής λειτουργίας. Περιμένετε πριν στείλετε άλλο μήνυμα."
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
@@ -355,7 +355,7 @@ msgstr "{count, plural, one {# bookmark} other {# bookmarks}}"
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# community found} other {# communities found}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
@@ -378,8 +378,6 @@ msgstr "{count, plural, one {# emoji} other {# emojis}}"
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# gift} other {# gifts}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# hour} other {# hours}}"
@@ -397,11 +395,6 @@ msgstr "{count, plural, one {# item} other {# items}}"
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# member} other {# members}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# minute} other {# minutes}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# mutual community} other {# mutual communities}}"
@@ -433,9 +426,7 @@ msgstr "{count, plural, one {# reactor} other {# reactors}}"
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# saved media item} other {# saved media items}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# second} other {# seconds}}"
@@ -667,11 +658,6 @@ msgstr "{duration} ahead of you"
msgid "{duration} behind you"
msgstr "{duration} behind you"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "{durationLabel} slowmode"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2847,7 +2833,7 @@ msgstr "Admins paused invites — you can't join right now."
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6546,8 +6532,8 @@ msgstr "Channel type: {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "Channel updated"
@@ -7162,7 +7148,7 @@ msgstr "Clear attachment mocks"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "Clear command"
@@ -13509,7 +13495,7 @@ msgstr "Enter the SMS code"
msgid "Enter this code in your browser to complete sign-in."
msgstr "Enter this code in your browser to complete sign-in."
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "enter to <0><1>save</1></0>"
@@ -13650,7 +13636,7 @@ msgstr "Escape key"
msgid "Escape key exits keyboard mode"
msgstr "Escape key exits keyboard mode"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "escape to <0><1>cancel</1></0>"
@@ -22465,7 +22451,7 @@ msgstr "Objects"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26668,7 +26654,7 @@ msgid "Reset premium state"
msgstr "Reset premium state"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "Reset slider to default value"
@@ -29896,7 +29882,7 @@ msgid "slash {commandName}"
msgstr "slash {commandName}"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "Slider value"
@@ -29968,7 +29954,7 @@ msgid "Slow profile load"
msgstr "Slow profile load"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "Slowmode"
@@ -29984,21 +29970,37 @@ msgstr "Slowmode · wait {remaining}"
msgid "Slowmode active"
msgstr "Slowmode active"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "Slowmode is enabled for this channel."
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "Slowmode is active ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "Slowmode is on, but you're immune."
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "Slowmode is enabled"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "Slowmode is on – wait {duration} before sending another."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "Slowmode is set to {durationLabel} for this channel."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "Slowmode is set to {durationLabel}, but you are immune."
# auto-i18n: reviewed unchanged
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "Slowmode is set to {durationLabel}. Wait before sending another message."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36912,7 +36914,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "Please wait a moment. This window will close automatically once we receive your message."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
@@ -37416,7 +37418,6 @@ msgstr "What should people call you?"
msgid "What to include"
msgstr "What to include"
# auto-i18n: reviewed unchanged
#. Description for a media search slash-command query.
#: src/features/devtools/hooks/useCommands.ts:74
msgid "What to search for."
@@ -38501,11 +38502,6 @@ msgstr "You're here, {username}! Good to have you with us."
msgid "You're in preview mode"
msgstr "You're in preview mode"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "You're in slowmode. Wait before sending another message."
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
@@ -38868,7 +38864,6 @@ msgstr "Your most recent purchase is no longer within the refund window. Contact
msgid "your new email"
msgstr "Your new email"
# auto-i18n: reviewed unchanged
#. Description for the /nick nickname option.
#: src/features/devtools/hooks/useCommands.ts:78
msgid "Your new nickname, or leave blank to reset it."
@@ -356,7 +356,7 @@ msgstr "{count, plural, one {# bookmark} other {# bookmarks}}"
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# community found} other {# communities found}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
@@ -379,8 +379,6 @@ msgstr "{count, plural, one {# emoji} other {# emojis}}"
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# gift} other {# gifts}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# hour} other {# hours}}"
@@ -398,11 +396,6 @@ msgstr "{count, plural, one {# item} other {# items}}"
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# member} other {# members}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# minute} other {# minutes}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# mutual community} other {# mutual communities}}"
@@ -434,9 +427,7 @@ msgstr "{count, plural, one {# reactor} other {# reactors}}"
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# saved media item} other {# saved media items}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# second} other {# seconds}}"
@@ -668,11 +659,6 @@ msgstr "{duration} ahead of you"
msgid "{duration} behind you"
msgstr "{duration} behind you"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "{durationLabel} slowmode"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2848,7 +2834,7 @@ msgstr "Admins paused invites — you can't join right now."
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6547,8 +6533,8 @@ msgstr "Channel type: {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "Channel updated"
@@ -7163,7 +7149,7 @@ msgstr "Clear attachment mocks"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "Clear command"
@@ -13510,7 +13496,7 @@ msgstr "Enter the SMS code"
msgid "Enter this code in your browser to complete sign-in."
msgstr "Enter this code in your browser to complete sign-in."
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "enter to <0><1>save</1></0>"
@@ -13651,7 +13637,7 @@ msgstr "Escape key"
msgid "Escape key exits keyboard mode"
msgstr "Escape key exits keyboard mode"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "escape to <0><1>cancel</1></0>"
@@ -22466,7 +22452,7 @@ msgstr "Objects"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26669,7 +26655,7 @@ msgid "Reset premium state"
msgstr "Reset premium state"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "Reset slider to default value"
@@ -29897,7 +29883,7 @@ msgid "slash {commandName}"
msgstr "slash {commandName}"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "Slider value"
@@ -29969,7 +29955,7 @@ msgid "Slow profile load"
msgstr "Slow profile load"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "Slowmode"
@@ -29985,21 +29971,36 @@ msgstr "Slowmode · wait {remaining}"
msgid "Slowmode active"
msgstr "Slowmode active"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "Slowmode is enabled for this channel."
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "Slowmode is active ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "Slowmode is enabled, but you are immune."
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "Slowmode is enabled"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "Slowmode is on — wait {duration} before sending another."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "Slowmode is set to {durationLabel} for this channel."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "Slowmode is set to {durationLabel}, but you are immune."
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "Slowmode is set to {durationLabel}. Wait before sending another message."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36913,7 +36914,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "Wait a moment. This window will close automatically once we receive your message."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
@@ -38501,11 +38502,6 @@ msgstr "You're here, {username}! Good to have you with us."
msgid "You're in preview mode"
msgstr "You're in preview mode"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "You're in slowmode. Wait before sending another message."
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
@@ -355,7 +355,7 @@ msgstr "{count, plural, one {# marcador} other {# marcadores}}"
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# comunidad encontrada} other {# comunidades encontradas}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# comunidad no está disponible temporalmente debido a una falla en el condensador de flujo.} other {# comunidades no están disponibles temporalmente debido a una falla en el condensador de flujo.}}"
@@ -378,8 +378,6 @@ msgstr "{count, plural, one {# emoji} other {# emojis}}"
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# regalo} other {# regalos}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# hora} other {# horas}}"
@@ -397,11 +395,6 @@ msgstr "{count, plural, one {# elemento} other {# elementos}}"
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# miembro} other {# miembros}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# minuto} other {# minutos}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# comunidad en común} other {# comunidades en común}}"
@@ -433,9 +426,7 @@ msgstr "{count, plural, one {# reacción} other {# reacciones}}"
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# elemento multimedia guardado} other {# elementos multimedia guardados}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# segundo} other {# segundos}}"
@@ -667,11 +658,6 @@ msgstr "{duration} más que tú"
msgid "{duration} behind you"
msgstr "A {duration} de ti"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "modo lento de {durationLabel}"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2847,7 +2833,7 @@ msgstr "Los administradores pausaron las invitaciones; no puedes unirte ahora."
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6546,8 +6532,8 @@ msgstr "Tipo de canal: {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "Canal actualizado"
@@ -7162,7 +7148,7 @@ msgstr "Borrar simulaciones de adjuntos"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "Borrar comando"
@@ -13509,7 +13495,7 @@ msgstr "Ingresa el código SMS"
msgid "Enter this code in your browser to complete sign-in."
msgstr "Ingresa este código en tu navegador para completar el inicio de sesión."
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "enter para <0><1>guardar</1></0>"
@@ -13650,7 +13636,7 @@ msgstr "tecla Escape"
msgid "Escape key exits keyboard mode"
msgstr "La tecla Escape sale del modo teclado"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "escape para <0><1>cancelar</1></0>"
@@ -22465,7 +22451,7 @@ msgstr "Objetos"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26668,7 +26654,7 @@ msgid "Reset premium state"
msgstr "Restablecer estado premium"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "Restablecer el deslizador al valor predeterminado"
@@ -29896,7 +29882,7 @@ msgid "slash {commandName}"
msgstr "barra {commandName}"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "Valor del deslizador"
@@ -29968,7 +29954,7 @@ msgid "Slow profile load"
msgstr "Carga lenta de perfil"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "Modo lento"
@@ -29984,21 +29970,36 @@ msgstr "Modo lento · espera {remaining}"
msgid "Slowmode active"
msgstr "Modo lento activado"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "El modo lento está activado para este canal."
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "Modo lento activo ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "El modo lento está activado, pero eres inmune."
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "Modo lento habilitado"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "Modo lento activado: espera {duration} antes de enviar otro mensaje."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "El modo lento está configurado en {durationLabel} para este canal."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "El modo lento está configurado en {durationLabel}, pero eres inmune."
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "El modo lento está configurado en {durationLabel}. Espera antes de enviar otro mensaje."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36912,7 +36913,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "Espera un momento. Esta ventana se cerrará automáticamente cuando recibamos tu mensaje."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "Tiempo de espera entre mensajes. \"{bypassSlowmodePermissionLabel}\" puede omitirlo."
@@ -38500,11 +38501,6 @@ msgstr "¡Llegaste, {username}! Qué bueno tenerte con nosotros."
msgid "You're in preview mode"
msgstr "Estás en modo de vista previa"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "Estás en modo lento. Espera antes de enviar otro mensaje."
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
@@ -355,7 +355,7 @@ msgstr "{count, plural, one {# marcador} other {# marcadores}}"
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# comunidad encontrada} other {# comunidades encontradas}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# comunidad no está disponible temporalmente debido a un fallo en el condensador de flujo.} other {# comunidades no están disponibles temporalmente debido a un fallo en el condensador de flujo.}}"
@@ -378,8 +378,6 @@ msgstr "{count, plural, one {# emoji} other {# emojis}}"
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# regalo} other {# regalos}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# hora} other {# horas}}"
@@ -397,11 +395,6 @@ msgstr "{count, plural, one {# elemento} other {# elementos}}"
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# miembro} other {# miembros}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# minuto} other {# minutos}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# comunidad en común} other {# comunidades en común}}"
@@ -433,9 +426,7 @@ msgstr "{count, plural, one {# reacción} other {# reacciones}}"
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# elemento multimedia guardado} other {# elementos multimedia guardados}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# segundo} other {# segundos}}"
@@ -667,11 +658,6 @@ msgstr "{duration} más que tú"
msgid "{duration} behind you"
msgstr "A {duration} de ti"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "Modo lento de {durationLabel}"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2847,7 +2833,7 @@ msgstr "Los administradores han pausado las invitaciones; no puedes unirte ahora
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6546,8 +6532,8 @@ msgstr "Tipo de canal: {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "Canal actualizado"
@@ -7162,7 +7148,7 @@ msgstr "Borrar simulaciones de adjuntos"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "Borrar comando"
@@ -13509,7 +13495,7 @@ msgstr "Introduce el código SMS"
msgid "Enter this code in your browser to complete sign-in."
msgstr "Introduce este código en tu navegador para completar el inicio de sesión."
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "pulsa para <0><1>guardar</1></0>"
@@ -13650,7 +13636,7 @@ msgstr "Tecla Escape"
msgid "Escape key exits keyboard mode"
msgstr "La tecla Escape sale del modo teclado"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "pulsa escape para <0><1>cancelar</1></0>"
@@ -22465,7 +22451,7 @@ msgstr "Objetos"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26668,7 +26654,7 @@ msgid "Reset premium state"
msgstr "Restablecer estado premium"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "Restablecer el deslizador a su valor predeterminado"
@@ -29896,7 +29882,7 @@ msgid "slash {commandName}"
msgstr "barra {commandName}"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "Valor del deslizador"
@@ -29968,7 +29954,7 @@ msgid "Slow profile load"
msgstr "Carga lenta del perfil"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "Modo lento"
@@ -29984,21 +29970,36 @@ msgstr "Modo lento · espera {remaining}"
msgid "Slowmode active"
msgstr "Modo lento activo"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "El modo lento está activado en este canal."
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "Modo lento activo ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "El modo lento está activado, pero eres inmune."
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "Modo lento activado"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "El modo lento está activado. Espera {duration} antes de enviar otro mensaje."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "El modo lento está configurado en {durationLabel} para este canal."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "El modo lento está configurado en {durationLabel}, pero eres inmune."
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "El modo lento está configurado en {durationLabel}. Espera antes de enviar otro mensaje."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36912,7 +36913,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "Espera un momento. Esta ventana se cerrará automáticamente cuando recibamos tu mensaje."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "Tiempo de espera entre mensajes. \"{bypassSlowmodePermissionLabel}\" puede omitirlo."
@@ -38500,11 +38501,6 @@ msgstr "¡Ya estás aquí, {username}! Nos alegra tenerte con nosotros."
msgid "You're in preview mode"
msgstr "Estás en modo de vista previa"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "Estás en modo lento. Espera antes de enviar otro mensaje."
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
@@ -355,7 +355,7 @@ msgstr "{count, plural, one {# kirjanmerkki} other {# kirjanmerkkiä}}"
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# yhteisö löytyi} other {# yhteisöä löytyi}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# yhteisö on tilapäisesti pois käytöstä toimintahäiriön vuoksi.} other {# yhteisöä on tilapäisesti pois käytöstä toimintahäiriön vuoksi.}}"
@@ -378,8 +378,6 @@ msgstr "{count, plural, one {# emoji} other {# emojia}}"
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# lahja} other {# lahjaa}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# tunti} other {# tuntia}}"
@@ -397,11 +395,6 @@ msgstr "{count, plural, one {# kohde} other {# kohdetta}}"
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# jäsen} other {# jäsentä}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# minuutti} other {# minuuttia}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# yhteinen yhteisö} other {# yhteistä yhteisöä}}"
@@ -433,9 +426,7 @@ msgstr "{count, plural, one {# reagoija} other {# reagoijaa}}"
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# tallennettu media} other {# tallennettua mediaa}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# sekunti} other {# sekuntia}}"
@@ -667,11 +658,6 @@ msgstr "sinua {duration} edellä"
msgid "{duration} behind you"
msgstr "{duration} sinua jäljessä"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "{durationLabel} hidastettu tila"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2847,7 +2833,7 @@ msgstr "Ylläpitäjät keskeyttivät kutsut – et voi liittyä juuri nyt."
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6546,8 +6532,8 @@ msgstr "Kanavan tyyppi: {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "Kanava päivitetty"
@@ -7162,7 +7148,7 @@ msgstr "Tyhjennä liitteiden mallit"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "Tyhjennä komento"
@@ -13509,7 +13495,7 @@ msgstr "Anna tekstiviestikoodi"
msgid "Enter this code in your browser to complete sign-in."
msgstr "Kirjoita tämä koodi selaimeesi kirjautumisen viimeistelemiseksi."
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "tallenna painamalla <0><1>enter</1></0>"
@@ -13650,7 +13636,7 @@ msgstr "Esc-näppäin"
msgid "Escape key exits keyboard mode"
msgstr "Esc-näppäin poistuu näppäimistötilasta"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "palaa ja <0><1>peruuta</1></0>"
@@ -22465,7 +22451,7 @@ msgstr "Esineet"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26668,7 +26654,7 @@ msgid "Reset premium state"
msgstr "Nollaa premium-tila"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "Palauta liukusäädin oletusarvoon"
@@ -29896,7 +29882,7 @@ msgid "slash {commandName}"
msgstr "vinokomento {commandName}"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "Liukusäätimen arvo"
@@ -29968,7 +29954,7 @@ msgid "Slow profile load"
msgstr "Hidas profiilin lataus"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "Hidas tila"
@@ -29984,21 +29970,36 @@ msgstr "Hidas tila · odota {remaining}"
msgid "Slowmode active"
msgstr "Hidas tila käytössä"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "Hidas tila on käytössä tällä kanavalla."
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "Hidas tila aktiivinen ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "Hidas tila on käytössä, mutta sinuun se ei vaikuta."
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "Hidas tila käytössä"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "Hidas tila on käytössä – odota {duration} ennen kuin lähetät uuden viestin."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "Hidas tila on asetettu arvoon {durationLabel} tälle kanavalle."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "Hidas tila on asetettu arvoon {durationLabel}, mutta sinulla on vapautus."
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "Hidas tila on asetettu arvoon {durationLabel}. Odota ennen kuin lähetät uuden viestin."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36912,7 +36913,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "Odota hetki. Tämä ikkuna sulkeutuu automaattisesti, kun olemme vastaanottaneet viestisi."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "Viive viestien välillä. \"{bypassSlowmodePermissionLabel}\" voi ohittaa sen."
@@ -38500,11 +38501,6 @@ msgstr "Tervetuloa, {username}! Mukava nähdä sinua täällä."
msgid "You're in preview mode"
msgstr "Esikatselutila käytössä"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "Olet hidastetussa tilassa. Odota ennen seuraavan viestin lähettämistä."
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
@@ -355,7 +355,7 @@ msgstr "{count, plural, one {# signet} other {# signets}}"
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# communauté trouvée} other {# communautés trouvées}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# communauté est temporairement indisponible en raison d'un dysfonctionnement du condensateur de flux.} other {# communautés sont temporairement indisponibles en raison d'un dysfonctionnement du condensateur de flux.}}"
@@ -378,8 +378,6 @@ msgstr "{count, plural, one {# emoji} other {# emojis}}"
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# cadeau} other {# cadeaux}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# heure} other {# heures}}"
@@ -397,11 +395,6 @@ msgstr "{count, plural, one {# élément} other {# éléments}}"
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# membre} other {# membres}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# minute} other {# minutes}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# communauté en commun} other {# communautés en commun}}"
@@ -433,9 +426,7 @@ msgstr "{count, plural, one {# personne a réagi} other {# personnes ont réagi}
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# élément multimédia enregistré} other {# éléments multimédias enregistrés}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# seconde} other {# secondes}}"
@@ -667,11 +658,6 @@ msgstr "{duration} d'avance sur vous"
msgid "{duration} behind you"
msgstr "{duration} de décalage avec vous"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "Mode lent {durationLabel}"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2847,7 +2833,7 @@ msgstr "Les admins ont désactivé les invitations. Vous ne pouvez pas rejoindre
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6546,8 +6532,8 @@ msgstr "Type de canal : {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "Chaîne mise à jour"
@@ -7162,7 +7148,7 @@ msgstr "Effacer les maquettes de pièces jointes"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "Effacer la commande"
@@ -13509,7 +13495,7 @@ msgstr "Saisissez le code SMS"
msgid "Enter this code in your browser to complete sign-in."
msgstr "Saisissez ce code dans votre navigateur pour terminer la connexion."
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "appuyez sur Entrée pour <0><1>enregistrer</1></0>"
@@ -13650,7 +13636,7 @@ msgstr "Touche Échap"
msgid "Escape key exits keyboard mode"
msgstr "La touche Échap quitte le mode clavier"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "Échap pour <0><1>annuler</1></0>"
@@ -22465,7 +22451,7 @@ msgstr "Objets"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26668,7 +26654,7 @@ msgid "Reset premium state"
msgstr "Réinitialiser l'état premium"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "Réinitialiser le curseur à sa valeur par défaut"
@@ -29896,7 +29882,7 @@ msgid "slash {commandName}"
msgstr "/ {commandName}"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "Valeur du curseur"
@@ -29968,7 +29954,7 @@ msgid "Slow profile load"
msgstr "Chargement lent du profil"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "Mode lent"
@@ -29984,21 +29970,36 @@ msgstr "Mode lent · attendez {remaining}"
msgid "Slowmode active"
msgstr "Mode lent activé"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "Le mode lent est activé pour ce canal."
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "Mode lent actif ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "Le mode lent est activé, mais vous êtes immunisé."
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "Mode lent activé"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "Le mode lent est activé — attendez {duration} avant d'envoyer un autre message."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "Le mode lent est réglé sur {durationLabel} pour ce canal."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "Le mode lent est réglé sur {durationLabel}, mais vous en êtes exempt."
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "Le mode lent est réglé sur {durationLabel}. Attendez avant d'envoyer un autre message."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36912,7 +36913,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "Veuillez patienter un instant. Cette fenêtre se fermera automatiquement dès que nous recevrons votre message."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "Attendre entre les messages. « {bypassSlowmodePermissionLabel} » peut contourner cette limite."
@@ -38500,11 +38501,6 @@ msgstr "Vous êtes là, {username} ! Content de vous avoir parmi nous."
msgid "You're in preview mode"
msgstr "Vous êtes en mode aperçu"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "Vous êtes en mode lent. Attendez avant d'envoyer un autre message."
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
@@ -355,7 +355,7 @@ msgstr "{count, plural, one {# סימנייה} other {# סימניות}}"
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# קהילה נמצאה} other {# קהילות נמצאו}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# קהילה אינה זמינה באופן זמני עקב תקלה בקבלן השטף.} other {# קהילות אינן זמינות באופן זמני עקב תקלה בקבלן השטף.}}"
@@ -378,8 +378,6 @@ msgstr "{count, plural, one {# אימוג'י} other {# אימוג'יז}}"
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# מתנה} other {# מתנות}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# שעה} other {# שעות}}"
@@ -397,11 +395,6 @@ msgstr "{count, plural, one {# פריט} other {# פריטים}}"
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# חבר} other {# חברים}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# דקה} other {# דקות}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# קהילה משותפת} other {# קהילות משותפות}}"
@@ -433,9 +426,7 @@ msgstr "{count, plural, one {# מגיב} other {# מגיבים}}"
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# פריט מדיה שמור} other {# פריטי מדיה שמורים}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# שנייה} other {# שניות}}"
@@ -667,11 +658,6 @@ msgstr "{duration} לפניך"
msgid "{duration} behind you"
msgstr "{duration} אחריך"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "מצב איטי: {durationLabel}"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2847,7 +2833,7 @@ msgstr "מנהלים השהו הזמנות – אינך יכול/ה להצטרף
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6546,8 +6532,8 @@ msgstr "סוג ערוץ: {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "הערוץ עודכן"
@@ -7162,7 +7148,7 @@ msgstr "מחיקת הדמיות קבצים מצורפים"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "נקה פקודה"
@@ -13509,7 +13495,7 @@ msgstr "הזן את קוד ה-SMS"
msgid "Enter this code in your browser to complete sign-in."
msgstr "הזן קוד זה בדפדפן שלך כדי להשלים את הכניסה."
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "Enter כדי <0><1>לשמור</1></0>"
@@ -13650,7 +13636,7 @@ msgstr "מקש Escape"
msgid "Escape key exits keyboard mode"
msgstr "מקש Escape יוצא ממצב מקלדת"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "הקש Escape כדי <0><1>לבטל</1></0>"
@@ -22465,7 +22451,7 @@ msgstr "אובייקטים"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26668,7 +26654,7 @@ msgid "Reset premium state"
msgstr "איפוס מצב פרימיום"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "איפוס המחוון לערך ברירת המחדל"
@@ -29896,7 +29882,7 @@ msgid "slash {commandName}"
msgstr "קו נטוי {commandName}"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "ערך המחוון"
@@ -29968,7 +29954,7 @@ msgid "Slow profile load"
msgstr "טעינת פרופיל איטית"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "מצב איטי"
@@ -29984,21 +29970,36 @@ msgstr "מצב איטי · המתן {remaining}"
msgid "Slowmode active"
msgstr "מצב איטי פעיל"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "מצב איטי מופעל בערוץ זה."
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "מצב איטי פעיל ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "מצב איטי מופעל, אך אתה חסין."
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "מצב איטי מופעל"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "מצב איטי מופעל – המתן {duration} לפני שליחת הודעה נוספת."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "מצב איטי מוגדר ל-{durationLabel} עבור ערוץ זה."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "מצב איטי מוגדר ל-{durationLabel}, אך אתה פטור."
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "מצב איטי מוגדר ל-{durationLabel}. המתן לפני שליחת הודעה נוספת."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36912,7 +36913,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "המתן רגע. חלון זה ייסגר אוטומטית ברגע שנקבל את ההודעה שלך."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "המתנה בין הודעות. \"{bypassSlowmodePermissionLabel}\" יכול לעקוף זאת."
@@ -38500,11 +38501,6 @@ msgstr "היי, {username}! כיף שבאת."
msgid "You're in preview mode"
msgstr "אתה במצב תצוגה מקדימה"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "אתה במצב איטי. המתן לפני שליחת הודעה נוספת."
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
@@ -355,7 +355,7 @@ msgstr "{count, plural, one {# बुकमार्क} other {# बुकम
msgid "{count, plural, one {# community found} other {# communities found}}"
msgstr "{count, plural, one {# समुदाय मिला} other {# समुदाय मिले}}"
#: src/features/app/components/layout/GuildsLayout.tsx:902
#: src/features/app/components/layout/GuildsLayout.tsx:907
msgid "{count, plural, one {# community is temporarily unavailable due to a flux capacitor malfunction.} other {# communities are temporarily unavailable due to a flux capacitor malfunction.}}"
msgstr "{count, plural, one {# समुदाय फ्लक्स कैपेसिटर में खराबी के कारण अस्थायी रूप से अनुपलब्ध है।} other {# समुदाय फ्लक्स कैपेसिटर में खराबी के कारण अस्थायी रूप से अनुपलब्ध हैं।}}"
@@ -378,8 +378,6 @@ msgstr "{count, plural, one {# इमोजी} other {# इमोजी}}"
msgid "{count, plural, one {# gift} other {# gifts}}"
msgstr "{count, plural, one {# उपहार} other {# उपहार}}"
#. Slowmode duration expressed in hours, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:41
#: src/features/channel/components/modals/GroupInvitesBottomSheet.tsx:139
msgid "{count, plural, one {# hour} other {# hours}}"
msgstr "{count, plural, one {# घंटा} other {# घंटे}}"
@@ -397,11 +395,6 @@ msgstr "{count, plural, one {# आइटम} other {# आइटम}}"
msgid "{count, plural, one {# member} other {# members}}"
msgstr "{count, plural, one {# सदस्य} other {# सदस्य}}"
#. Slowmode duration expressed in minutes, shown on the channel settings slowmode slider.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:37
msgid "{count, plural, one {# minute} other {# minutes}}"
msgstr "{count, plural, one {# मिनट} other {# मिनट}}"
#: src/features/channel/components/direct_message/DMWelcomeSection.tsx:246
msgid "{count, plural, one {# mutual community} other {# mutual communities}}"
msgstr "{count, plural, one {# साझा समुदाय} other {# साझा समुदाय}}"
@@ -433,9 +426,7 @@ msgstr "{count, plural, one {# रिएक्शन} other {# रिएक्
msgid "{count, plural, one {# saved media item} other {# saved media items}}"
msgstr "{count, plural, one {# सहेजी गई मीडिया आइटम} other {# सहेजी गई मीडिया आइटम}}"
#. Slowmode duration expressed in seconds, shown on the channel settings slowmode slider.
#: src/features/channel/components/channel_header_components/developer_tools/MockingMenu.tsx:183
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:33
msgid "{count, plural, one {# second} other {# seconds}}"
msgstr "{count, plural, one {# सेकंड} other {# सेकंड}}"
@@ -667,11 +658,6 @@ msgstr "आपसे {duration} आगे"
msgid "{duration} behind you"
msgstr "आपसे {duration} पीछे"
#. Short label in the channel and chat slowmode indicator. Keep it concise. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:31
msgid "{durationLabel} slowmode"
msgstr "{durationLabel} स्लोमोड"
#. Gift duration label. PREMIUM_PRODUCT_NAME is the paid plan name.
#: src/features/gift/utils/GiftUtils.ts:12
msgid "{durationQuantity, plural, one {# day} other {# days}} of {premiumProductName}"
@@ -2847,7 +2833,7 @@ msgstr "एडमिन ने इनवाइट रोक दिए हैं
#. Disclosure button label in the voice connection status popout. Reveals technical bandwidth and network stats.
#. User settings tab for power-user and experimental application preferences.
#. Voice participant context menu submenu label for IDs and diagnostics.
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:337
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:336
#: src/features/guild/components/modals/guild_tabs/guild_overview_tab/index.tsx:28
#: src/features/ui/action_menu/items/VoiceParticipantMenuData.tsx:178
#: src/features/user/components/settings_utils/SettingsConstants.tsx:145
@@ -6546,8 +6532,8 @@ msgstr "चैनल का प्रकार: {0}."
#. Audit log entry label. Past-tense action describing that a channel was edited.
#: src/features/app/config/AuditLogConstants.ts:16
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:207
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:226
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:206
#: src/features/channel/components/modals/channel_tabs/ChannelOverviewTab.tsx:225
msgid "Channel updated"
msgstr "चैनल अपडेट किया गया"
@@ -7162,7 +7148,7 @@ msgstr "अटैचमेंट मॉक साफ़ करें"
#. Accessible label for the button that clears the slash command being composed in the message box.
#. Accessible label for the composer button that clears the slash command currently being composed.
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:126
#: src/features/channel/components/LexicalChannelTextareaContent.tsx:130
#: src/features/channel/components/SlashCommandParamBar.tsx:16
msgid "Clear command"
msgstr "कमांड साफ़ करें"
@@ -13509,7 +13495,7 @@ msgstr "एसएमएस कोड डालें"
msgid "Enter this code in your browser to complete sign-in."
msgstr "साइन-इन पूरा करने के लिए अपने ब्राउज़र में यह कोड डालें।"
#: src/features/channel/components/EditingMessageInput.tsx:359
#: src/features/channel/components/EditingMessageInput.tsx:358
msgid "enter to <0><1>save</1></0>"
msgstr "सेव करने के लिए <0><1>एंटर करें</1></0>"
@@ -13650,7 +13636,7 @@ msgstr "एस्केप कुंजी"
msgid "Escape key exits keyboard mode"
msgstr "एस्केप कुंजी से कीबोर्ड मोड से बाहर निकलें"
#: src/features/channel/components/EditingMessageInput.tsx:346
#: src/features/channel/components/EditingMessageInput.tsx:345
msgid "escape to <0><1>cancel</1></0>"
msgstr "रद्द करने के लिए <0><1>एस्केप</1></0> करें"
@@ -22465,7 +22451,7 @@ msgstr "ऑब्जेक्ट"
#. Option label for an H.264 backup stream select.
#: src/features/channel/components/ChannelReplyBar.tsx:62
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/MatureContentSection.tsx:26
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:23
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:24
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:97
#: src/features/guild/components/modals/guild_tabs/GuildModerationTab.tsx:139
#: src/features/user/components/modals/tabs/advanced_settings_tab/AdvancedVideoControls.tsx:123
@@ -26668,7 +26654,7 @@ msgid "Reset premium state"
msgstr "प्रीमियम स्थिति रीसेट करें"
#. Accessible label for a button that resets a slider to its default value.
#: src/features/ui/components/Slider.tsx:18
#: src/features/ui/components/Slider.tsx:24
msgid "Reset slider to default value"
msgstr "स्लाइडर को डिफ़ॉल्ट मान पर रीसेट करें"
@@ -29896,7 +29882,7 @@ msgid "slash {commandName}"
msgstr "स्लैश {commandName}"
#. Accessible label announcing the current slider value.
#: src/features/ui/components/Slider.tsx:22
#: src/features/ui/components/Slider.tsx:28
msgid "Slider value"
msgstr "स्लाइडर मान"
@@ -29968,7 +29954,7 @@ msgid "Slow profile load"
msgstr "प्रोफ़ाइल धीमी गति से लोड करें"
#. Channel overview settings tab label, control, or validation message (name, topic, slowmode, voice region, mature content gate).
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:18
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:19
msgid "Slowmode"
msgstr "स्लोमोड"
@@ -29984,21 +29970,36 @@ msgstr "स्लोमोड · {remaining} प्रतीक्षा कर
msgid "Slowmode active"
msgstr "स्लोमोड सक्रिय है"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:27
msgid "Slowmode is enabled for this channel."
msgstr "इस चैनल के लिए स्लोमोड चालू है।"
#. Short label in the composer slowmode indicator while the countdown runs. Keep it concise. Preserve {remaining}; it is an mm:ss or hh:mm:ss timer inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:38
msgid "Slowmode is active ({remaining})"
msgstr "स्लोमोड सक्रिय है ({remaining})"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is enabled, but you are immune."
msgstr "स्लोमोड चालू है, लेकिन आप पर इसका असर नहीं होगा।"
#. Short label in the composer slowmode indicator when the reader is free to send. Keep it concise.
#: src/features/channel/components/SlowmodeIndicator.tsx:34
msgid "Slowmode is enabled"
msgstr "स्लोमोड सक्रिय है"
#. Modal body shown when slowmode blocks sending a message. {duration} is a localized short duration such as "2 minutes".
#: src/features/slowmode/components/alerts/SlowmodeRateLimitedModal.tsx:17
msgid "Slowmode is on — wait {duration} before sending another."
msgstr "स्लोमोड चालू है — दूसरा मैसेज भेजने से पहले {duration} इंतज़ार करें."
#. Tooltip on the composer slowmode indicator when the reader is free to send. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:29
msgid "Slowmode is set to {durationLabel} for this channel."
msgstr "इस चैनल के लिए स्लोमोड {durationLabel} पर सेट है."
#. Tooltip on the composer slowmode indicator when the reader can bypass slowmode. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:19
msgid "Slowmode is set to {durationLabel}, but you are immune."
msgstr "स्लोमोड {durationLabel} पर सेट है, लेकिन आप इससे मुक्त हैं."
#. Tooltip on the composer slowmode indicator while the reader is counting down. Preserve {durationLabel}; it is inserted by code.
#: src/features/channel/components/SlowmodeIndicator.tsx:24
msgid "Slowmode is set to {durationLabel}. Wait before sending another message."
msgstr "स्लोमोड {durationLabel} पर सेट है. दूसरा मैसेज भेजने से पहले इंतज़ार करें."
#. Developer tools debug menu label. Internal-only surface for developers; translators may keep this terse.
#: src/features/channel/components/channel_header_components/developer_tools/DeveloperOptionLabels.ts:253
msgid "Slowmode remaining"
@@ -36912,7 +36913,7 @@ msgid "Wait a moment. This window will close automatically once we receive your
msgstr "थोड़ा इंतज़ार करें. आपका मैसेज मिलते ही यह विंडो अपने-आप बंद हो जाएगी."
#. Description under the slowmode slider in channel settings. bypassSlowmodePermissionLabel is the localized Bypass Slowmode permission name.
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:28
#: src/features/channel/components/modals/channel_tabs/channel_overview_tab/SlowmodeControl.tsx:29
msgid "Wait between messages. \"{bypassSlowmodePermissionLabel}\" can bypass it."
msgstr "संदेशों के बीच प्रतीक्षा करें। \"{bypassSlowmodePermissionLabel}\" इसे बायपास कर सकता है।"
@@ -38500,11 +38501,6 @@ msgstr "आप आ गए, {username}! आपको हमारे साथ
msgid "You're in preview mode"
msgstr "आप प्रीव्यू मोड में हैं"
#. Description text in the channel and chat slowmode indicator.
#: src/features/channel/components/SlowmodeIndicator.tsx:23
msgid "You're in slowmode. Wait before sending another message."
msgstr "आप स्लोमोड में हैं। दूसरा मैसेज भेजने से पहले इंतज़ार करें।"
#. Subtitle below the connected-call header confirming the user is connected.
#: src/features/voice/components/bottomsheets/DirectCallLobbyBottomSheet.tsx:108
msgid "You're in the call"
Loaded 100 of 387 files, more files were not shown because too many files have changed in this diff. Show more