Compare commits

...
78 changed files with 2435 additions and 485 deletions

No files matched your search

+1
View File
@@ -1,5 +1,6 @@
self-hosted-runner:
labels:
- fluxer-desktop-macos-arm64
- fluxer-electron-build
config-variables: null
@@ -0,0 +1,108 @@
name: build desktop Electron
on:
workflow_dispatch:
inputs:
prebuilt_release_tag:
description: Draft release containing prebuilt Electron distributions. Leave empty to compile Electron on the build runner instead.
required: false
default: desktop-electron-runtime-44.4.1-fluxer.1
type: string
permissions:
contents: read
concurrency:
group: desktop-electron-build
cancel-in-progress: false
jobs:
build:
name: Build patched Electron (linux-${{ matrix.arch }})
if: inputs.prebuilt_release_tag == ''
runs-on: [self-hosted, Linux, X64, fluxer-electron-build]
timeout-minutes: 1440
strategy:
fail-fast: false
max-parallel: 1
matrix:
arch: [x64, arm64]
steps:
- name: Checkout source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Build patched Electron distribution
env:
CONTAINER_ENGINE: podman
CONTAINER_CPUS: "8"
CONTAINER_MEMORY: 48g
BUILD_JOBS: "8"
run: >-
bash fluxer_desktop/electron/build-linux.sh
--arch "${{ matrix.arch }}"
--build-root /srv/fluxer-electron-build/work
--output-dir "${{ runner.temp }}/fluxer-electron-artifacts"
- name: Upload patched Electron distribution
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: desktop-electron-linux-${{ matrix.arch }}
path: |
${{ runner.temp }}/fluxer-electron-artifacts/electron-v44.4.1-linux-${{ matrix.arch }}.zip
${{ runner.temp }}/fluxer-electron-artifacts/electron-v44.4.1-linux-${{ matrix.arch }}.fluxer.json
if-no-files-found: error
retention-days: 30
compression-level: 0
import:
name: Import patched Electron (linux-${{ matrix.arch }})
if: inputs.prebuilt_release_tag != ''
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: write
strategy:
fail-fast: false
matrix:
arch: [x64, arm64]
steps:
- name: Checkout source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
- name: Set up Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 26
- name: Download and verify build artifacts
env:
GH_TOKEN: ${{ github.token }}
PREBUILT_RELEASE_TAG: ${{ inputs.prebuilt_release_tag }}
ELECTRON_ARCH: ${{ matrix.arch }}
FLUXER_ELECTRON_DIST: ${{ runner.temp }}/fluxer-electron-artifacts
shell: bash
run: |
set -euo pipefail
gh release view "$PREBUILT_RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
--json isDraft --jq '.isDraft' | grep -qx true
gh release download "$PREBUILT_RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
--pattern "electron-v44.4.1-linux-$ELECTRON_ARCH.zip" \
--pattern "electron-v44.4.1-linux-$ELECTRON_ARCH.fluxer.json" \
--dir "$FLUXER_ELECTRON_DIST"
node <<'NODE'
const {verifyLinuxElectronArchive} = require('./fluxer_desktop/scripts/custom-electron.cjs');
verifyLinuxElectronArchive(process.env.FLUXER_ELECTRON_DIST, process.env.ELECTRON_ARCH)
.then(() => console.log('Patched Electron artifact verified.'))
.catch((error) => { console.error(error); process.exitCode = 1; });
NODE
- name: Upload patched Electron distribution
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
name: desktop-electron-linux-${{ matrix.arch }}
path: |
${{ runner.temp }}/fluxer-electron-artifacts/electron-v44.4.1-linux-${{ matrix.arch }}.zip
${{ runner.temp }}/fluxer-electron-artifacts/electron-v44.4.1-linux-${{ matrix.arch }}.fluxer.json
if-no-files-found: error
retention-days: 30
compression-level: 0
+40
View File
@@ -21,6 +21,11 @@ on:
required: false
default: ""
type: string
electron_build_run_id:
description: Successful manual main-branch build desktop Electron workflow run supplying the patched Linux Electron distributions. Required when building Linux.
required: false
default: ""
type: string
shell_contract:
description: "Did the shell contract change since the oldest shell these modules still reach? unchanged refuses if it did. compatible keeps older shells on these modules because the renderer still works on them. raise stops sending these modules to shells without the new contract: Linux and managed shells keep their current renderer, Windows and macOS update their shell first."
required: true
@@ -51,6 +56,7 @@ jobs:
timeout-minutes: 25
permissions:
contents: read
actions: read
outputs:
version: ${{ steps.meta.outputs.version }}
pub_date: ${{ steps.meta.outputs.pub_date }}
@@ -64,6 +70,30 @@ jobs:
ref: main
fetch-depth: 0
- name: Validate patched Electron build
env:
ELECTRON_BUILD_RUN_ID: ${{ inputs.electron_build_run_id }}
SKIP_TARGETS: ${{ inputs.skip_targets }}
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
skipped=",$(printf '%s' "$SKIP_TARGETS" | tr '[:space:]' ','),"
if [[ "$skipped" == *,linux,* ]] || \
{ [[ "$skipped" == *,linux-x64,* ]] && [[ "$skipped" == *,linux-arm64,* ]]; }; then
exit 0
fi
if ! [[ "$ELECTRON_BUILD_RUN_ID" =~ ^[1-9][0-9]*$ ]]; then
echo 'Provide electron_build_run_id from a successful build desktop Electron workflow.' >&2
exit 1
fi
if ! gh api "repos/$GITHUB_REPOSITORY/actions/runs/$ELECTRON_BUILD_RUN_ID" \
--jq 'select(.status == "completed" and .conclusion == "success" and .event == "workflow_dispatch" and .head_branch == "main" and .repository.full_name != null and .head_repository.full_name == .repository.full_name and .path == ".github/workflows/build-desktop-electron.yaml") | .id' \
| grep -qx "$ELECTRON_BUILD_RUN_ID"; then
echo 'Use a successful manual build desktop Electron run from this repository on main.' >&2
exit 1
fi
- name: Set up Rust toolchain (CI helpers)
uses: dtolnay/rust-toolchain@02cb101ec7c40f2c49e1d9714d64511d8e1b74de
with:
@@ -613,12 +643,22 @@ jobs:
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
--step analyse_velopack_paths
- name: Download patched Electron distribution (Linux)
if: matrix.platform == 'linux'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
github-token: ${{ github.token }}
run-id: ${{ inputs.electron_build_run_id }}
name: desktop-electron-linux-${{ matrix.arch }}
path: ${{ runner.temp }}/fluxer-electron-dist
- name: Build Electron app (Linux)
if: matrix.platform == 'linux'
working-directory: ${{ env.WORKDIR }}/fluxer_desktop
env:
BUILD_CHANNEL: ${{ env.BUILD_CHANNEL }}
USE_SYSTEM_FPM: true
FLUXER_ELECTRON_DIST: ${{ runner.temp }}/fluxer-electron-dist
run: >-
cargo run --locked --quiet --manifest-path ${{ github.workspace }}/_ci/tools/ci/Cargo.toml -- build-desktop
--step build_app_linux
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {TestAccount} from '@app/api/auth/tests/AuthTestUtils';
import {createTestAccount, setUserACLs} from '@app/api/auth/tests/AuthTestUtils';
import {getConfig} from '@app/api/Config';
import {getInstanceConfigRepository, getLimitConfigService} from '@app/api/middleware/ServiceSingletons';
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
import {HTTP_STATUS} from '@app/api/test/TestConstants';
import {createBuilder} from '@app/api/test/TestRequestBuilder';
import {AdminACLs} from '@fluxer/constants/src/AdminACLs';
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes';
import type {LimitRule} from '@fluxer/limits/src/LimitTypes';
import type {LimitConfigGetResponse} from '@fluxer/schema/src/domains/admin/AdminSchemas';
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
import type {z} from 'zod';
type LimitConfigResponse = z.infer<typeof LimitConfigGetResponse>;
const REMOVED_KEY = 'max_created_packs';
describe.each([true, false])('limit config with a key from an older release (self-hosted: %s)', (selfHosted) => {
let harness: ApiTestHarness;
let originalSelfHosted: boolean;
beforeAll(async () => {
originalSelfHosted = getConfig().instance.selfHosted;
getConfig().instance.selfHosted = selfHosted;
harness = await createApiTestHarness();
});
beforeEach(async () => {
await harness.reset();
await getLimitConfigService().refreshCache();
});
afterAll(async () => {
await harness.shutdown();
getConfig().instance.selfHosted = originalSelfHosted;
});
async function createAdmin(): Promise<TestAccount> {
return await setUserACLs(harness, await createTestAccount(harness), [
AdminACLs.AUTHENTICATE,
AdminACLs.INSTANCE_LIMIT_CONFIG_VIEW,
AdminACLs.INSTANCE_LIMIT_CONFIG_UPDATE,
]);
}
async function storeRuleFromOlderRelease(): Promise<void> {
const config = getLimitConfigService().getConfigSnapshot();
const legacyRule = {
id: 'vip',
filters: {traits: ['vip']},
limits: {max_bio_length: 200, [REMOVED_KEY]: 5},
} as LimitRule;
await getInstanceConfigRepository().setLimitConfig({
traitDefinitions: [...config.traitDefinitions, 'vip'],
rules: [...config.rules, legacyRule],
});
await getLimitConfigService().refreshCache();
}
it('leaves the removed key out when reading the stored config', async () => {
const admin = await createAdmin();
await storeRuleFromOlderRelease();
const response = await createBuilder<LimitConfigResponse>(harness, admin.token)
.get('/admin/limit-config')
.execute();
const vip = response.limit_config.rules.find((rule) => rule.id === 'vip');
expect(vip?.limits).toEqual({max_bio_length: 200});
});
it('saves the config it read back and drops the removed key from storage', async () => {
const admin = await createAdmin();
await storeRuleFromOlderRelease();
const {limit_config: config} = await createBuilder<LimitConfigResponse>(harness, admin.token)
.get('/admin/limit-config')
.execute();
await createBuilder<LimitConfigResponse>(harness, admin.token)
.put('/admin/limit-config')
.body({
limit_config: {
traitDefinitions: config.traitDefinitions,
rules: config.rules.map(({modifiedFields: _modifiedFields, ...rule}) => rule),
},
})
.execute();
const {config: stored} = await getInstanceConfigRepository().readLimitConfigInputs();
const vip = stored?.rules.find((rule) => rule.id === 'vip');
expect(vip?.limits).toEqual({max_bio_length: 200});
});
it('drops a rule whose only limits were removed keys', async () => {
const admin = await createAdmin();
const config = getLimitConfigService().getConfigSnapshot();
await getInstanceConfigRepository().setLimitConfig({
traitDefinitions: [...config.traitDefinitions, 'packs'],
rules: [...config.rules, {id: 'packs', filters: {traits: ['packs']}, limits: {[REMOVED_KEY]: 5}} as LimitRule],
});
await getLimitConfigService().refreshCache();
const {limit_config: read} = await createBuilder<LimitConfigResponse>(harness, admin.token)
.get('/admin/limit-config')
.execute();
expect(read.rules.some((rule) => rule.id === 'packs')).toBe(false);
await createBuilder<LimitConfigResponse>(harness, admin.token)
.put('/admin/limit-config')
.body({
limit_config: {
traitDefinitions: read.traitDefinitions,
rules: read.rules.map(({modifiedFields: _modifiedFields, ...rule}) => rule),
},
})
.execute();
});
it('names the unknown key in the validation error', async () => {
const admin = await createAdmin();
const {limit_config: config} = await createBuilder<LimitConfigResponse>(harness, admin.token)
.get('/admin/limit-config')
.execute();
const response = await createBuilder<{errors: Array<{path: string; code: string}>}>(harness, admin.token)
.put('/admin/limit-config')
.body({
limit_config: {
traitDefinitions: [...config.traitDefinitions, 'vip'],
rules: [
...config.rules.map(({modifiedFields: _modifiedFields, ...rule}) => rule),
{id: 'vip', filters: {traits: ['vip']}, limits: {max_bio_length: 200, [REMOVED_KEY]: 5}},
],
},
})
.expect(HTTP_STATUS.BAD_REQUEST, APIErrorCodes.INVALID_FORM_BODY)
.execute();
expect(response.errors).toEqual([
expect.objectContaining({
path: `limit_config.rules.${config.rules.length}.limits.${REMOVED_KEY}`,
code: ValidationErrorCodes.INVALID_FORMAT,
}),
]);
});
});
+12 -8
View File
@@ -103,7 +103,7 @@ export function mergeWithCurrentDefaults(
const mergedRules: Array<LimitRule> = [];
const existingRulesMap = new Map<string, LimitRule>();
for (const rule of stored.rules) {
existingRulesMap.set(rule.id, {...rule, limits: clampToAllowedRange(rule.limits)});
existingRulesMap.set(rule.id, {...rule, limits: readKnownLimits(rule.limits)});
}
for (const defaultRule of newDefaults.rules) {
const existingRule = existingRulesMap.get(defaultRule.id);
@@ -125,6 +125,9 @@ export function mergeWithCurrentDefaults(
existingRulesMap.delete(defaultRule.id);
}
for (const [, rule] of existingRulesMap) {
if (Object.keys(rule.limits).length === 0) {
continue;
}
const modifiedFields = findModifiedLimits(rule.limits, defaultBaseRule.limits);
mergedRules.push({
id: rule.id,
@@ -231,16 +234,17 @@ function wasSavedUnderOtherDefaults(
);
}
function clampToAllowedRange(limits: Partial<Record<LimitKey, number>>): Partial<Record<LimitKey, number>> {
const clamped = {...limits};
function readKnownLimits(limits: Partial<Record<LimitKey, number>>): Partial<Record<LimitKey, number>> {
const known: Partial<Record<LimitKey, number>> = {};
for (const key of LIMIT_KEYS) {
const value = clamped[key];
const {min = 0, max} = LIMIT_KEY_METADATA[key];
if (value !== undefined && max !== undefined) {
clamped[key] = Math.min(Math.max(value, min), max);
const value = limits[key];
if (value === undefined) {
continue;
}
const {min = 0, max} = LIMIT_KEY_METADATA[key];
known[key] = max === undefined ? value : Math.min(Math.max(value, min), max);
}
return clamped;
return known;
}
function pickModifiedLimits(
+1
View File
@@ -51,6 +51,7 @@ export const Routes = {
help: (): string | null => hostedMarketingUrl('help'),
helpArticle: (slug: string): string | null => hostedMarketingUrl(`help/${slug}`),
patchNotes: (): string | null => hostedMarketingUrl('patch-notes'),
feedback: (): string | null => (RuntimeConfig.isSelfHosted() ? null : 'https://feedback.fluxer.com'),
dmChannel: (channelId: string) => `/channels/@me/${channelId}`,
favoritesChannel: (channelId: string) => `/channels/@favorites/${channelId}`,
guildMembers: (guildId: string) => `/channels/${guildId}/members`,
@@ -30,6 +30,7 @@ import {
OPENS_IN_BROWSER_DESCRIPTOR,
SEARCH_SETTINGS_PLACEHOLDER_DESCRIPTOR,
SIGN_OUT_DESCRIPTOR,
SUBMIT_FEEDBACK_DESCRIPTOR,
WHATS_NEW_DESCRIPTOR,
} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {ComponentBus} from '@app/features/platform/utils/ComponentBus';
@@ -98,6 +99,7 @@ import {
ArrowRightIcon,
ArrowSquareOutIcon,
CaretRightIcon,
ChatCircleDotsIcon,
SignOutIcon,
SparkleIcon,
} from '@phosphor-icons/react';
@@ -288,6 +290,28 @@ const BreadcrumbTitle: React.FC<BreadcrumbTitleProps> = ({parentLabel, currentLa
</span>
</span>
);
const ExternalSidebarItemLabel: React.FC<{label: React.ReactNode; hint: React.ReactNode; 'data-flx': string}> = ({
label,
hint,
'data-flx': dataFlx,
}) => (
<span className={settingsModalStyles.externalTabLabel} data-flx={dataFlx}>
<span className={settingsModalStyles.externalTabLabelText} data-flx={`${dataFlx}-text`}>
{label}
</span>
<span className={settingsModalStyles.srOnly} data-flx={`${dataFlx}-screen-reader-hint`}>
{hint}
</span>
<ArrowSquareOutIcon
size={remFromPx(16)}
weight="bold"
className={settingsModalStyles.externalTabIcon}
aria-hidden="true"
data-flx={`${dataFlx}-external-icon`}
/>
</span>
);
export const DesktopSettingsView: React.FC<DesktopSettingsViewProps> = observer(
({
groupedSettingsTabs,
@@ -353,6 +377,7 @@ export const DesktopSettingsView: React.FC<DesktopSettingsViewProps> = observer(
ModalCommands.push(modal(() => <LogoutModal data-flx="app.desktop-settings-view.handle-logout.logout-modal" />));
};
const patchNotesUrl = Routes.patchNotes();
const feedbackUrl = Routes.feedback();
const handleTabSelect = (tabType: UserSettingsTabType) => {
if (checkUnsavedChanges()) return;
onTabSelect(tabType);
@@ -625,35 +650,30 @@ export const DesktopSettingsView: React.FC<DesktopSettingsViewProps> = observer(
<SettingsModalSidebarItem
icon={SparkleIcon}
label={
<span
className={settingsModalStyles.externalTabLabel}
<ExternalSidebarItemLabel
label={i18n._(WHATS_NEW_DESCRIPTOR)}
hint={i18n._(OPENS_IN_BROWSER_DESCRIPTOR)}
data-flx="app.desktop-settings-view.whats-new-label"
>
<span
className={settingsModalStyles.externalTabLabelText}
data-flx="app.desktop-settings-view.whats-new-label-text"
>
{i18n._(WHATS_NEW_DESCRIPTOR)}
</span>
<span
className={settingsModalStyles.srOnly}
data-flx="app.desktop-settings-view.whats-new-screen-reader-hint"
>
{i18n._(OPENS_IN_BROWSER_DESCRIPTOR)}
</span>
<ArrowSquareOutIcon
size={remFromPx(16)}
weight="bold"
className={settingsModalStyles.externalTabIcon}
aria-hidden="true"
data-flx="app.desktop-settings-view.whats-new-external-icon"
/>
</span>
/>
}
onClick={() => void openExternalUrl(patchNotesUrl)}
data-flx="app.desktop-settings-view.settings-modal-sidebar-item.whats-new"
/>
)}
{feedbackUrl && (
<SettingsModalSidebarItem
icon={ChatCircleDotsIcon}
label={
<ExternalSidebarItemLabel
label={i18n._(SUBMIT_FEEDBACK_DESCRIPTOR)}
hint={i18n._(OPENS_IN_BROWSER_DESCRIPTOR)}
data-flx="app.desktop-settings-view.submit-feedback-label"
/>
}
onClick={() => void openExternalUrl(feedbackUrl)}
data-flx="app.desktop-settings-view.settings-modal-sidebar-item.submit-feedback"
/>
)}
<SettingsModalSidebarItem
icon={SignOutIcon}
label={i18n._(SIGN_OUT_DESCRIPTOR)}
@@ -1,55 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import i18n from '@app/app/I18n';
import {GenericErrorModal} from '@app/features/app/components/alerts/GenericErrorModal';
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import * as AuthenticationCommands from '@app/features/auth/commands/AuthenticationCommands';
import {VerificationResult} from '@app/features/auth/commands/AuthenticationCommands';
import {currentInstanceTarget} from '@app/features/platform/transport/InstanceHTTP';
import {useResendVerificationEmail} from '@app/features/auth/hooks/useVerifyEmailAction';
import {Button} from '@app/features/ui/button/Button';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import * as ToastCommands from '@app/features/ui/commands/ToastCommands';
import {WarningAlert} from '@app/features/ui/warning_alert/WarningAlert';
import {msg} from '@lingui/core/macro';
import {Trans, useLingui} from '@lingui/react/macro';
import {Trans} from '@lingui/react/macro';
import {observer} from 'mobx-react-lite';
import type React from 'react';
import {useState} from 'react';
const VERIFICATION_EMAIL_SENT_PLEASE_CHECK_YOUR_INBOX_DESCRIPTOR = msg({
message: 'Verification email sent. Check your inbox.',
comment: 'Toast confirmation in the email verification alert after a verification email has been sent.',
});
const TOO_MANY_REQUESTS_TITLE_DESCRIPTOR = msg({
message: "You're going too fast",
comment: 'Title of the error modal shown when resending a verification email is rate-limited.',
});
const TOO_MANY_REQUESTS_MESSAGE_DESCRIPTOR = msg({
message: "You've requested too many verification emails. Please wait a moment and try again.",
comment: 'Body of the error modal shown when resending a verification email is rate-limited.',
});
const SEND_FAILED_TITLE_DESCRIPTOR = msg({
message: "Couldn't send the verification email",
comment: 'Title of the error modal shown when sending the verification email fails unexpectedly.',
});
const SEND_FAILED_MESSAGE_DESCRIPTOR = msg({
message: 'Something went wrong while sending your verification email. Please try again in a moment.',
comment: 'Body of the error modal shown when sending the verification email fails unexpectedly.',
});
function showVerificationEmailErrorModal(result: VerificationResult): void {
const isRateLimited = result === VerificationResult.RATE_LIMITED;
ModalCommands.push(
modal(() => (
<GenericErrorModal
title={i18n._(isRateLimited ? TOO_MANY_REQUESTS_TITLE_DESCRIPTOR : SEND_FAILED_TITLE_DESCRIPTOR)}
message={i18n._(isRateLimited ? TOO_MANY_REQUESTS_MESSAGE_DESCRIPTOR : SEND_FAILED_MESSAGE_DESCRIPTOR)}
data-flx="app.email-verification-alert.resend.generic-error-modal"
/>
)),
);
}
interface EmailVerificationAlertProps {
title?: React.ReactNode;
@@ -57,25 +14,7 @@ interface EmailVerificationAlertProps {
}
export const EmailVerificationAlert = observer(({title, children}: EmailVerificationAlertProps) => {
const {i18n} = useLingui();
const [isResending, setIsResending] = useState(false);
const handleResend = async () => {
if (isResending) return;
setIsResending(true);
const result = await AuthenticationCommands.resendVerificationEmail(currentInstanceTarget());
switch (result) {
case VerificationResult.SUCCESS:
ToastCommands.success(i18n._(VERIFICATION_EMAIL_SENT_PLEASE_CHECK_YOUR_INBOX_DESCRIPTOR));
break;
case VerificationResult.RATE_LIMITED:
showVerificationEmailErrorModal(VerificationResult.RATE_LIMITED);
break;
case VerificationResult.SERVER_ERROR:
showVerificationEmailErrorModal(VerificationResult.SERVER_ERROR);
break;
}
setIsResending(false);
};
const {isSending, resend} = useResendVerificationEmail();
if (!RuntimeConfig.emailsEnabled) {
return null;
}
@@ -86,9 +25,9 @@ export const EmailVerificationAlert = observer(({title, children}: EmailVerifica
<Button
variant="primary"
small
disabled={isResending}
submitting={isResending}
onClick={handleResend}
disabled={isSending}
submitting={isSending}
onClick={resend}
data-flx="app.email-verification-alert.button.resend"
>
<Trans>Resend email</Trans>
@@ -18,6 +18,7 @@ import {
OPENS_IN_BROWSER_DESCRIPTOR,
SETTINGS_DESCRIPTOR,
SIGN_OUT_DESCRIPTOR,
SUBMIT_FEEDBACK_DESCRIPTOR,
WHATS_NEW_DESCRIPTOR,
} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {isKeyboardActivationKey} from '@app/features/input/utils/KeyboardUtils';
@@ -61,6 +62,7 @@ import {
ArrowClockwiseIcon,
ArrowLeftIcon,
ArrowSquareOutIcon,
ChatCircleDotsIcon,
type IconWeight,
SignOutIcon,
SparkleIcon,
@@ -417,6 +419,7 @@ const MobileSettingsList = observer(
ModalCommands.push(modal(() => <LogoutModal data-flx="app.mobile-settings-view.handle-logout.logout-modal" />));
}, []);
const patchNotesUrl = Routes.patchNotes();
const feedbackUrl = Routes.feedback();
const categories = Object.entries(mobileVisibleTabs);
const lastCategoryIndex = categories.length - 1;
return (
@@ -498,6 +501,19 @@ const MobileSettingsList = observer(
<div className={styles.divider} data-flx="app.mobile-settings-view.mobile-settings-list.divider--2" />
</>
)}
{categoryIndex === lastCategoryIndex && feedbackUrl && (
<>
<MobileSettingsActionItem
icon={ChatCircleDotsIcon}
iconWeight="fill"
label={i18n._(SUBMIT_FEEDBACK_DESCRIPTOR)}
onClick={() => void openExternalUrl(feedbackUrl)}
external={true}
data-flx="app.mobile-settings-view.mobile-settings-list.mobile-settings-action-item.submit-feedback"
/>
<div className={styles.divider} data-flx="app.mobile-settings-view.mobile-settings-list.divider--3" />
</>
)}
{categoryIndex === lastCategoryIndex && (
<MobileSettingsDangerItem
icon={SignOutIcon}
@@ -10,6 +10,7 @@ interface StatusAction {
onClick: () => void;
variant?: React.ComponentProps<typeof Button>['variant'];
fitContent?: boolean;
submitting?: boolean;
}
interface StatusSlateProps {
@@ -45,7 +46,8 @@ export const StatusSlate: React.FC<StatusSlateProps> = observer(
variant={action.variant ?? 'primary'}
fitContent={action.fitContent ?? true}
onClick={action.onClick}
submitting={false}
disabled={action.submitting}
submitting={action.submitting ?? false}
data-flx="app.status-slate.button.click"
>
{action.text}
@@ -1,11 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {UserSettingsModal} from '@app/features/app/components/dialogs/LoadableSettingsModals';
import {Nagbar} from '@app/features/app/components/layout/Nagbar';
import {NagbarButton} from '@app/features/app/components/layout/NagbarButton';
import {NagbarContent} from '@app/features/app/components/layout/NagbarContent';
import {NAGBAR_TONES, NagbarToneKind} from '@app/features/app/components/layout/NagbarTones';
import {OPEN_SETTINGS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {useVerifyEmailAction} from '@app/features/auth/hooks/useVerifyEmailAction';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import {BouncedEmailChangeModal} from '@app/features/user/components/modals/BouncedEmailChangeModal';
@@ -32,6 +31,7 @@ const ADD_A_WORKING_EMAIL_BUTTON_DESCRIPTOR = msg({
export const EmailVerificationNagbar = observer(({isMobile}: {isMobile: boolean}) => {
const {i18n} = useLingui();
const user = Users.currentUser;
const verifyEmailAction = useVerifyEmailAction();
if (!user) {
return null;
}
@@ -43,19 +43,6 @@ export const EmailVerificationNagbar = observer(({isMobile}: {isMobile: boolean}
)),
);
};
const openUserSettings = () => {
ModalCommands.push(
modal(
() => (
<UserSettingsModal
initialTab="account_security"
data-flx="app.app-layout.nagbars.email-verification-nagbar.open-user-settings.user-settings-modal"
/>
),
'user-settings',
),
);
};
return (
<Nagbar
isMobile={isMobile}
@@ -81,10 +68,12 @@ export const EmailVerificationNagbar = observer(({isMobile}: {isMobile: boolean}
) : (
<NagbarButton
isMobile={isMobile}
onClick={openUserSettings}
data-flx="app.app-layout.nagbars.email-verification-nagbar.nagbar-button.open-user-settings"
onClick={verifyEmailAction.onClick}
disabled={verifyEmailAction.submitting}
submitting={verifyEmailAction.submitting}
data-flx="app.app-layout.nagbars.email-verification-nagbar.nagbar-button.verify-email"
>
{i18n._(OPEN_SETTINGS_DESCRIPTOR)}
{verifyEmailAction.text}
</NagbarButton>
)
}
@@ -480,6 +480,28 @@ describe('InstanceSnapshotStore.resolve on an official web page', () => {
expect(resolution.snapshot.webAppEndpoint).toBe('https://alias.example.test');
});
test('an alias host also takes over the gateway and media the canonical host serves', async () => {
stubBrowserDocument('https://gateway-alias.example.test', {servesOwnApi: true});
const canonical = discoveryDocument('canonical.example.test');
vi.stubGlobal(
'fetch',
vi.fn(async () =>
jsonResponse({
...canonical,
endpoints: {
...canonical.endpoints,
gateway: 'wss://canonical.example.test/gateway',
media: 'https://canonical.example.test/media',
},
}),
),
);
const resolution = await InstanceSnapshotStore.resolve({input: 'https://gateway-alias.example.test', signal: null});
expect(resolution.snapshot.gatewayEndpoint).toBe('wss://gateway-alias.example.test/gateway');
expect(resolution.snapshot.mediaEndpoint).toBe('https://gateway-alias.example.test/media');
expect(resolution.snapshot.staticCdnEndpoint).toBe('https://cdn.canonical.example.test');
});
test('a host that is not marked as serving its own API still refuses a foreign API declaration', async () => {
stubBrowserDocument('https://fluxer.com');
vi.stubGlobal('fetch', serveLikeProduction('https://web.fluxer.app'));
@@ -0,0 +1,129 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import i18n from '@app/app/I18n';
import {GenericErrorModal} from '@app/features/app/components/alerts/GenericErrorModal';
import {UserSettingsModal} from '@app/features/app/components/dialogs/LoadableSettingsModals';
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import * as AuthenticationCommands from '@app/features/auth/commands/AuthenticationCommands';
import {VerificationResult} from '@app/features/auth/commands/AuthenticationCommands';
import {VERIFY_EMAIL_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {currentInstanceTarget} from '@app/features/platform/transport/InstanceHTTP';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import * as ToastCommands from '@app/features/ui/commands/ToastCommands';
import {BouncedEmailChangeModal} from '@app/features/user/components/modals/BouncedEmailChangeModal';
import type {User} from '@app/features/user/models/User';
import Users from '@app/features/user/state/Users';
import {msg} from '@lingui/core/macro';
import {useLingui} from '@lingui/react/macro';
import {useState} from 'react';
const VERIFICATION_EMAIL_SENT_PLEASE_CHECK_YOUR_INBOX_DESCRIPTOR = msg({
message: 'Verification email sent. Check your inbox.',
comment: 'Toast confirmation shown after a verification email has been sent.',
});
const TOO_MANY_REQUESTS_TITLE_DESCRIPTOR = msg({
message: "You're going too fast",
comment: 'Title of the error modal shown when resending a verification email is rate-limited.',
});
const TOO_MANY_REQUESTS_MESSAGE_DESCRIPTOR = msg({
message: "You've requested too many verification emails. Please wait a moment and try again.",
comment: 'Body of the error modal shown when resending a verification email is rate-limited.',
});
const SEND_FAILED_TITLE_DESCRIPTOR = msg({
message: "Couldn't send the verification email",
comment: 'Title of the error modal shown when sending the verification email fails unexpectedly.',
});
const SEND_FAILED_MESSAGE_DESCRIPTOR = msg({
message: 'Something went wrong while sending your verification email. Please try again in a moment.',
comment: 'Body of the error modal shown when sending the verification email fails unexpectedly.',
});
const SEND_VERIFICATION_EMAIL_DESCRIPTOR = msg({
message: 'Send verification email',
comment:
'Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.',
});
const RESEND_EMAIL_DESCRIPTOR = msg({
message: 'Resend email',
comment: 'Button label that sends another verification email after one was already sent. Keep it concise.',
});
function showVerificationEmailErrorModal(result: VerificationResult): void {
const isRateLimited = result === VerificationResult.RATE_LIMITED;
ModalCommands.push(
modal(() => (
<GenericErrorModal
title={i18n._(isRateLimited ? TOO_MANY_REQUESTS_TITLE_DESCRIPTOR : SEND_FAILED_TITLE_DESCRIPTOR)}
message={i18n._(isRateLimited ? TOO_MANY_REQUESTS_MESSAGE_DESCRIPTOR : SEND_FAILED_MESSAGE_DESCRIPTOR)}
data-flx="auth.verify-email-action.resend.generic-error-modal"
/>
)),
);
}
export function canResendVerificationEmail(user: User | null | undefined): boolean {
return Boolean(
RuntimeConfig.emailsEnabled &&
user?.isClaimed() &&
!RuntimeConfig.usesUsernameSignIn &&
user.email &&
!user.verified,
);
}
export function useResendVerificationEmail() {
const {i18n} = useLingui();
const [isSending, setIsSending] = useState(false);
const [sent, setSent] = useState(false);
const resend = async () => {
if (isSending) return;
setIsSending(true);
const result = await AuthenticationCommands.resendVerificationEmail(currentInstanceTarget());
switch (result) {
case VerificationResult.SUCCESS:
setSent(true);
ToastCommands.success(i18n._(VERIFICATION_EMAIL_SENT_PLEASE_CHECK_YOUR_INBOX_DESCRIPTOR));
break;
case VerificationResult.RATE_LIMITED:
showVerificationEmailErrorModal(VerificationResult.RATE_LIMITED);
break;
case VerificationResult.SERVER_ERROR:
showVerificationEmailErrorModal(VerificationResult.SERVER_ERROR);
break;
}
setIsSending(false);
};
return {isSending, sent, resend};
}
function openAccountSecuritySettings(): void {
ModalCommands.push(
modal(
() => <UserSettingsModal initialTab="account_security" data-flx="auth.verify-email-action.user-settings-modal" />,
'user-settings',
),
);
}
function openBouncedEmailChange(): void {
ModalCommands.push(
modal(() => <BouncedEmailChangeModal data-flx="auth.verify-email-action.bounced-email-change-modal" />),
);
}
export function useVerifyEmailAction() {
const {i18n} = useLingui();
const {isSending, sent, resend} = useResendVerificationEmail();
const user = Users.currentUser;
if (user?.emailBounced === true) {
return {text: i18n._(VERIFY_EMAIL_DESCRIPTOR), onClick: openBouncedEmailChange, submitting: false};
}
if (!canResendVerificationEmail(user)) {
return {text: i18n._(VERIFY_EMAIL_DESCRIPTOR), onClick: openAccountSecuritySettings, submitting: false};
}
return {
text: i18n._(sent ? RESEND_EMAIL_DESCRIPTOR : SEND_VERIFICATION_EMAIL_DESCRIPTOR),
onClick: () => void resend(),
submitting: isSending,
};
}
@@ -1,24 +1,20 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {UserSettingsModal} from '@app/features/app/components/dialogs/LoadableSettingsModals';
import {getActiveInstanceProductName} from '@app/features/app/state/ActiveInstanceProductName';
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import {openClaimAccountModal} from '@app/features/auth/components/modals/ClaimAccountModal';
import {useVerifyEmailAction} from '@app/features/auth/hooks/useVerifyEmailAction';
import styles from '@app/features/channel/components/barriers/BarrierComponents.module.css';
import {openChannelFollowModal} from '@app/features/channel/components/modals/ChannelFollowModal';
import wrapperStyles from '@app/features/channel/components/textarea/InputWrapper.module.css';
import textareaStyles from '@app/features/channel/components/textarea/TextareaInput.module.css';
import {FOLLOW_DESCRIPTOR} from '@app/features/channel/utils/ChannelFollowUtils';
import {CLAIM_ACCOUNT_DESCRIPTOR, VERIFY_EMAIL_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {CLAIM_ACCOUNT_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
import {unblockUser} from '@app/features/relationship/utils/RelationshipActionUtils';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import {BouncedEmailChangeModal} from '@app/features/user/components/modals/BouncedEmailChangeModal';
import {openMessagingPausedHelpModal} from '@app/features/user/components/modals/MessagingPausedHelpModal';
import Users from '@app/features/user/state/Users';
import {ACCOUNT_LIMITED_NOTICE_DESCRIPTOR} from '@app/features/user/utils/AccountLimitUtils';
import {TEXT_DID_NOT_GO_THROUGH_DESCRIPTOR} from '@app/features/user/utils/MessagingPausedHelpEmail';
import {msg} from '@lingui/core/macro';
@@ -168,7 +164,7 @@ export const UnclaimedAccountBarrier = observer(() => {
);
});
export const UnverifiedEmailBarrier = observer(() => {
const {i18n} = useLingui();
const verifyEmailAction = useVerifyEmailAction();
return (
<BarrierBase
message={<Trans>You need to verify your email to send messages in this community.</Trans>}
@@ -182,26 +178,12 @@ export const UnverifiedEmailBarrier = observer(() => {
action={
<Button
small={true}
onClick={() => {
const bounced = Users.currentUser?.emailBounced === true;
ModalCommands.push(
modal(
() =>
bounced ? (
<BouncedEmailChangeModal data-flx="channel.barriers.barrier-components.unverified-email-barrier.bounced-email-change-modal" />
) : (
<UserSettingsModal
initialTab="account_security"
data-flx="channel.barriers.barrier-components.unverified-email-barrier.user-settings-modal"
/>
),
bounced ? undefined : 'user-settings',
),
);
}}
onClick={verifyEmailAction.onClick}
disabled={verifyEmailAction.submitting}
submitting={verifyEmailAction.submitting}
data-flx="channel.barriers.barrier-components.unverified-email-barrier.button.action"
>
{i18n._(VERIFY_EMAIL_DESCRIPTOR)}
{verifyEmailAction.text}
</Button>
}
data-flx="channel.barriers.barrier-components.unverified-email-barrier.barrier-base"
@@ -1,19 +1,17 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {UserSettingsModal} from '@app/features/app/components/dialogs/LoadableSettingsModals';
import {StatusSlate} from '@app/features/app/components/dialogs/shared/StatusSlate';
import {EXAMPLE_FLUXER_TAG_FULL, EXAMPLE_USERNAME} from '@app/features/app/config/I18nDisplayConstants';
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import {openClaimAccountModal} from '@app/features/auth/components/modals/ClaimAccountModal';
import {useVerifyEmailAction} from '@app/features/auth/hooks/useVerifyEmailAction';
import styles from '@app/features/channel/components/direct_message/AddFriendForm.module.css';
import {CLAIM_ACCOUNT_DESCRIPTOR, VERIFY_EMAIL_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {CLAIM_ACCOUNT_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {failureCode, failureMessage} from '@app/features/platform/utils/ResponseInspection';
import * as RelationshipCommands from '@app/features/relationship/commands/RelationshipCommands';
import {getSendFriendRequestErrorMessage} from '@app/features/relationship/utils/RelationshipActionUtils';
import {OUTGOING_FRIEND_REQUEST_STATUS_DESCRIPTOR} from '@app/features/relationship/utils/RelationshipMessageDescriptors';
import {Button} from '@app/features/ui/button/Button';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import {Input} from '@app/features/ui/components/form/FormInput';
import MobileLayout from '@app/features/ui/state/MobileLayout';
import Users from '@app/features/user/state/Users';
@@ -59,6 +57,7 @@ export const AddFriendForm: React.FC<AddFriendFormProps> = observer(({onSuccess}
const [resultStatus, setResultStatus] = useState<'success' | 'error' | null>(null);
const [errorCode, setErrorCode] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const verifyEmailAction = useVerifyEmailAction();
const currentUser = Users.currentUser;
const usernameOnly = RuntimeConfig.usesUniqueUsernames;
const isClaimed = currentUser?.isClaimed() ?? true;
@@ -87,19 +86,9 @@ export const AddFriendForm: React.FC<AddFriendFormProps> = observer(({onSuccess}
description={<Trans>You need to verify your email address before you can send friend requests.</Trans>}
actions={[
{
text: i18n._(VERIFY_EMAIL_DESCRIPTOR),
onClick: () =>
ModalCommands.push(
modal(
() => (
<UserSettingsModal
initialTab="account_security"
data-flx="channel.direct-message.add-friend-form.on-click.user-settings-modal"
/>
),
'user-settings',
),
),
text: verifyEmailAction.text,
onClick: verifyEmailAction.onClick,
submitting: verifyEmailAction.submitting,
variant: 'primary',
},
]}
@@ -9,8 +9,10 @@ import {MEDIA_DESCRIPTOR, PERSONAL_NOTES_DESCRIPTOR} from '@app/features/i18n/ut
import {compareChannelPosition} from '@app/features/messaging/utils/ChannelShared';
import {AnnouncementChannelIcon} from '@app/features/ui/components/icons/AnnouncementChannelIcon';
import {EncryptedVoiceChannelIcon} from '@app/features/ui/components/icons/EncryptedVoiceChannelIcon';
import {ForumChannelIcon} from '@app/features/ui/components/icons/ForumChannelIcon';
import {LinkChannelIcon} from '@app/features/ui/components/icons/LinkChannelIcon';
import {LockedAnnouncementChannelIcon} from '@app/features/ui/components/icons/LockedAnnouncementChannelIcon';
import {LockedForumChannelIcon} from '@app/features/ui/components/icons/LockedForumChannelIcon';
import {LockedLinkChannelIcon} from '@app/features/ui/components/icons/LockedLinkChannelIcon';
import {LockedTextChannelIcon} from '@app/features/ui/components/icons/LockedTextChannelIcon';
import {LockedVoiceChannelIcon} from '@app/features/ui/components/icons/LockedVoiceChannelIcon';
@@ -26,14 +28,7 @@ import * as NicknameUtils from '@app/features/user/utils/NicknameUtils';
import {FLUXERBOT_ID} from '@fluxer/constants/src/AppConstants';
import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants';
import {msg} from '@lingui/core/macro';
import {
CaretDownIcon,
ChatsIcon,
ChatsTeardropIcon,
type IconProps,
ImagesSquareIcon,
NotePencilIcon,
} from '@phosphor-icons/react';
import {CaretDownIcon, ChatsIcon, type IconProps, ImagesSquareIcon, NotePencilIcon} from '@phosphor-icons/react';
const VOICE_MATURE_DESCRIPTOR = msg({
message: 'Voice (mature)',
@@ -103,6 +98,7 @@ function isChannelRoleRequired(channel: {
return (overwrite.deny & Permissions.CONNECT) === Permissions.CONNECT;
case ChannelTypes.GUILD_TEXT:
case ChannelTypes.GUILD_ANNOUNCEMENT:
case ChannelTypes.GUILD_FORUM:
case ChannelTypes.GUILD_LINK:
return (overwrite.deny & Permissions.VIEW_CHANNEL) === Permissions.VIEW_CHANNEL;
default:
@@ -182,7 +178,9 @@ export function getIcon(
case ChannelTypes.PRIVATE_THREAD:
return <ChatsIcon data-flx="channel.channel-utils.get-icon.thread-icon" {...props} />;
case ChannelTypes.GUILD_FORUM:
return <ChatsTeardropIcon data-flx="channel.channel-utils.get-icon.forum-icon" {...props} />;
if (roleRequired)
return <LockedForumChannelIcon data-flx="channel.channel-utils.get-icon.forum-icon" {...props} />;
return <ForumChannelIcon data-flx="channel.channel-utils.get-icon.forum-icon" {...props} />;
case ChannelTypes.GUILD_MEDIA:
return <ImagesSquareIcon data-flx="channel.channel-utils.get-icon.media-icon" {...props} />;
default:
@@ -2,15 +2,15 @@
import i18n from '@app/app/I18n';
import {showDmActionErrorModal} from '@app/features/app/components/alerts/DmActionErrorModal';
import {UserSettingsModal} from '@app/features/app/components/dialogs/LoadableSettingsModals';
import {StatusSlate} from '@app/features/app/components/dialogs/shared/StatusSlate';
import {openClaimAccountModal} from '@app/features/auth/components/modals/ClaimAccountModal';
import {useVerifyEmailAction} from '@app/features/auth/hooks/useVerifyEmailAction';
import * as PrivateChannelCommands from '@app/features/channel/commands/PrivateChannelCommands';
import type {UnaddableRecipient} from '@app/features/channel/components/modals/UnaddableRecipientsConfirmModal';
import {UnaddableRecipientsConfirmModal} from '@app/features/channel/components/modals/UnaddableRecipientsConfirmModal';
import type {Channel} from '@app/features/channel/models/Channel';
import {getDuplicateGroupDMChannels, getMaxGroupDmOtherRecipients} from '@app/features/channel/utils/GroupDmUtils';
import {CLAIM_ACCOUNT_DESCRIPTOR, VERIFY_EMAIL_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {CLAIM_ACCOUNT_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {focusChannelTextareaAfterNavigation} from '@app/features/messaging/utils/ChannelTextareaFocusUtils';
import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands';
import {HttpError} from '@app/features/platform/types/EndpointError';
@@ -150,6 +150,7 @@ export const CreateDMRestrictionSlate: React.FC<
> = ({restriction, fullHeight = true, ...props}) => {
const {i18n: lingui} = useLingui();
const isUnclaimed = restriction === 'unclaimed';
const verifyEmailAction = useVerifyEmailAction();
return (
<div data-flx="channel.create-dm-modal-utils.create-dm-restriction-slate.div" {...props}>
<StatusSlate
@@ -157,27 +158,18 @@ export const CreateDMRestrictionSlate: React.FC<
title={lingui._(isUnclaimed ? CLAIM_YOUR_ACCOUNT_DESCRIPTOR : VERIFY_YOUR_EMAIL_DESCRIPTOR)}
description={getCreateDMRestrictionMessage(lingui, restriction)}
actions={[
{
text: lingui._(isUnclaimed ? CLAIM_ACCOUNT_DESCRIPTOR : VERIFY_EMAIL_DESCRIPTOR),
onClick: () => {
if (isUnclaimed) {
openClaimAccountModal({force: true});
return;
isUnclaimed
? {
text: lingui._(CLAIM_ACCOUNT_DESCRIPTOR),
onClick: () => openClaimAccountModal({force: true}),
variant: 'primary',
}
ModalCommands.push(
modal(
() => (
<UserSettingsModal
initialTab="account_security"
data-flx="channel.create-dm-modal-utils.restriction-slate.user-settings-modal"
/>
),
'user-settings',
),
);
},
variant: 'primary',
},
: {
text: verifyEmailAction.text,
onClick: verifyEmailAction.onClick,
submitting: verifyEmailAction.submitting,
variant: 'primary',
},
]}
fullHeight={fullHeight}
data-flx="channel.create-dm-modal-utils.create-dm-restriction-slate.status-slate"
@@ -86,7 +86,7 @@ export const GuildCreateForm = observer(() => {
);
return () => modalFooterContext?.setFooterContent(null);
}, [handleSubmit, isSubmitting, modalFooterContext, nameValue]);
const accountGate = renderGuildCreateAccountGate(i18n, 'guild.add-guild-modal.guild-create-form');
const accountGate = renderGuildCreateAccountGate('guild.add-guild-modal.guild-create-form');
if (accountGate) {
return accountGate;
}
@@ -2,12 +2,12 @@
import {Routes} from '@app/app/Routes';
import {showGenericErrorModal} from '@app/features/app/components/alerts/GenericErrorModalCommands';
import {UserSettingsModal} from '@app/features/app/components/dialogs/LoadableSettingsModals';
import {ExternalLink} from '@app/features/app/components/shared/ExternalLink';
import {AVATAR_RECOMMENDED_SIZE_LABEL, STATIC_IMAGE_FORMATS} from '@app/features/app/config/I18nDisplayConstants';
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import {GlobalLimits} from '@app/features/app/utils/GlobalLimits';
import {openClaimAccountModal} from '@app/features/auth/components/modals/ClaimAccountModal';
import {useVerifyEmailAction} from '@app/features/auth/hooks/useVerifyEmailAction';
import {AssetCropModal, AssetType} from '@app/features/expressions/components/modals/AssetCropModal';
import {openAssetSourceModal} from '@app/features/expressions/components/modals/AssetSourceModal';
import {isAnimatedFile} from '@app/features/expressions/utils/AnimatedImageUtils';
@@ -23,7 +23,6 @@ import {
import {
FAILED_TO_PROCESS_CROPPED_IMAGE_DESCRIPTOR,
INVALID_IMAGE_TRY_ANOTHER_DESCRIPTOR,
VERIFY_EMAIL_DESCRIPTOR,
} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {openFilePicker} from '@app/features/messaging/utils/FilePickerUtils';
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
@@ -149,7 +148,21 @@ export function useGuildCreateIconField({
return {previewIconUrl, iconName, handleOpenIconUpload, handleClearIcon};
}
export function renderGuildCreateAccountGate(i18n: I18n, dataFlx: string): React.ReactNode {
const GuildCreateVerifyEmailButton = ({dataFlx}: {dataFlx: string}) => {
const verifyEmailAction = useVerifyEmailAction();
return (
<Button
onClick={verifyEmailAction.onClick}
disabled={verifyEmailAction.submitting}
submitting={verifyEmailAction.submitting}
data-flx={`${dataFlx}.button.push`}
>
{verifyEmailAction.text}
</Button>
);
};
export function renderGuildCreateAccountGate(dataFlx: string): React.ReactNode {
const currentUser = Users.currentUser;
if (currentUser != null && !currentUser.isClaimed()) {
return (
@@ -177,19 +190,10 @@ export function renderGuildCreateAccountGate(i18n: I18n, dataFlx: string): React
<p data-flx={`${dataFlx}.p--2`}>
<Trans>You need to verify your email address before you can create a community.</Trans>
</p>
<Button
onClick={() =>
ModalCommands.push(
modal(
() => <UserSettingsModal initialTab="account_security" data-flx={`${dataFlx}.user-settings-modal`} />,
'user-settings',
),
)
}
data-flx={`${dataFlx}.button.push`}
>
{i18n._(VERIFY_EMAIL_DESCRIPTOR)}
</Button>
<GuildCreateVerifyEmailButton
dataFlx={dataFlx}
data-flx="guild.add-guild-modal.guild-create-shared.render-guild-create-account-gate.guild-create-verify-email-button"
/>
</div>
</div>
);
@@ -407,7 +407,7 @@ export const TemplateImportForm = observer(() => {
urlValue,
jsonValue,
]);
const accountGate = renderGuildCreateAccountGate(i18n, 'guild.add-guild-modal.template-import-form');
const accountGate = renderGuildCreateAccountGate('guild.add-guild-modal.template-import-form');
if (accountGate) {
return accountGate;
}
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "تعذّر إرسال طلب الصداقة. حاول مرة أخرى."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "تعذّر إرسال رسالة التحقق عبر البريد الإلكتروني"
@@ -27910,8 +27910,10 @@ msgstr "إعادة الإرسال ({secondsRemaining}ث)"
msgid "Resend code"
msgstr "إعادة إرسال الرمز"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "إعادة إرسال البريد الإلكتروني"
@@ -29939,6 +29941,11 @@ msgstr "إرسال رسائل تحويل النص إلى كلام"
msgid "Send verification code"
msgstr "إرسال رمز التحقق"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "إرسال بريد التحقق"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "حدث خطأ أثناء حفظ هذا الإعداد. الرجاء المحاولة مرة أخرى بعد قليل."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "حدث خطأ أثناء إرسال رسالة التحقق عبر البريد الإلكتروني. يُرجى المحاولة مرة أخرى بعد قليل."
@@ -32734,6 +32741,11 @@ msgstr "قائمة فرعية"
msgid "Submit application"
msgstr "إرسال الطلب"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "إرسال ملاحظات"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "جارٍ الإرسال"
@@ -37744,8 +37756,8 @@ msgstr "رمز التحقق"
msgid "Verification code sent"
msgstr "تم إرسال رمز التحقق"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "تم إرسال رسالة تحقق إلى بريدك الإلكتروني. تحقق من صندوق الوارد."
@@ -40176,7 +40188,7 @@ msgstr "أنت تعدّل الرسائل بسرعة كبيرة"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "لقد تلقيت هدية"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "لقد طلبت عددًا كبيرًا جدًا من رسائل البريد الإلكتروني للتحقق. يُرجى الانتظار لحظة والمحاولة مرة أخرى."
@@ -1427,6 +1427,7 @@
"Send messages in threads",
"Send messages, images, GIFs and emoji",
"Send report",
"Send verification email",
"Sends both channels of a stereo microphone. Needs all processing off and a channel at 64 kbps or higher.",
"Sequential send",
"Serial send",
@@ -1521,6 +1522,7 @@
"Stopped requiring a tag on new posts",
"Stream in 4K at 60 FPS",
"Stream quality",
"Submit feedback",
"Subscribe",
"Subscribe now. Your remaining gift time is added after your paid period.",
"Subscriber badge on your profile",
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Поканата за приятелство не можа да бъде изпратена. Опитай отново."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Неуспешно изпращане на имейл за потвърждение"
@@ -27910,8 +27910,10 @@ msgstr "Изпрати отново ({secondsRemaining}с)"
msgid "Resend code"
msgstr "Изпрати кода отново"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Изпрати имейла отново"
@@ -29939,6 +29941,11 @@ msgstr "Изпращане на TTS съобщения"
msgid "Send verification code"
msgstr "Изпращане на код за потвърждение"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Изпрати имейл за потвърждение"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Нещо се обърка при запазването на тази настройка. Опитай отново след малко."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Нещо се обърка при изпращането на имейла за потвърждение. Опитай отново след малко."
@@ -32734,6 +32741,11 @@ msgstr "Подменю"
msgid "Submit application"
msgstr "Изпращане на заявление"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Изпратете обратна връзка"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Изпращане"
@@ -37744,8 +37756,8 @@ msgstr "Код за потвърждение"
msgid "Verification code sent"
msgstr "Кодът за потвърждение е изпратен"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Изпратен е имейл за потвърждение. Провери входящата си поща."
@@ -40176,7 +40188,7 @@ msgstr "Редактираш съобщения твърде бързо"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Получи подарък"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Поиска твърде много имейли за потвърждение. Изчакай малко и опитай отново."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Nepodařilo se odeslat žádost o přátelství. Zkuste to znovu."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Ověřovací e-mail se nepodařilo odeslat"
@@ -27910,8 +27910,10 @@ msgstr "Znovu odeslat ({secondsRemaining} s)"
msgid "Resend code"
msgstr "Znovu odeslat kód"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Znovu odeslat e-mail"
@@ -29939,6 +29941,11 @@ msgstr "Odesílat zprávy převodem textu na řeč"
msgid "Send verification code"
msgstr "Odeslat ověřovací kód"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Odeslat ověřovací e-mail"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Při ukládání tohoto nastavení se něco pokazilo. Zkuste to prosím za chvíli znovu."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Při odesílání ověřovacího e-mailu se něco pokazilo. Zkuste to prosím za chvíli znovu."
@@ -32734,6 +32741,11 @@ msgstr "Podnabídka"
msgid "Submit application"
msgstr "Odeslat žádost"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Odeslat zpětnou vazbu"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Odesílání"
@@ -37744,8 +37756,8 @@ msgstr "Ověřovací kód"
msgid "Verification code sent"
msgstr "Ověřovací kód odeslán"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Ověřovací e-mail byl odeslán. Zkontrolujte si doručenou poštu."
@@ -40176,7 +40188,7 @@ msgstr "Zprávy upravujete příliš rychle"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Dostali jste dárek"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Vyžádali jste si příliš mnoho ověřovacích e-mailů. Počkejte chvíli a zkuste to znovu."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Kunne ikke sende venneanmodningen. Prøv igen."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Kunne ikke sende bekræftelsesmailen"
@@ -27910,8 +27910,10 @@ msgstr "Send igen ({secondsRemaining} sek.)"
msgid "Resend code"
msgstr "Send kode igen"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Send mail igen"
@@ -29939,6 +29941,11 @@ msgstr "Send TTS-beskeder"
msgid "Send verification code"
msgstr "Send bekræftelseskode"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Send bekræftelsesmail"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Der opstod en fejl under forsøget på at gemme denne indstilling. Prøv venligst igen om et øjeblik."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Der opstod en fejl under afsendelse af din bekræftelsesmail. Prøv venligst igen om et øjeblik."
@@ -32734,6 +32741,11 @@ msgstr "Undermenu"
msgid "Submit application"
msgstr "Indsend ansøgning"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Send feedback"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Sender"
@@ -37744,8 +37756,8 @@ msgstr "Bekræftelseskode"
msgid "Verification code sent"
msgstr "Bekræftelseskode sendt"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Bekræftelsesmail sendt. Tjek din indbakke."
@@ -40176,7 +40188,7 @@ msgstr "Du redigerer beskeder for hurtigt"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Du har modtaget en gave"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Du har anmodet om for mange bekræftelsesmails. Vent et øjeblik, og prøv igen."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Freundschaftsanfrage konnte nicht gesendet werden. Versuche es noch einmal."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Verifizierungs-E-Mail konnte nicht gesendet werden"
@@ -27910,8 +27910,10 @@ msgstr "Erneut senden ({secondsRemaining}s)"
msgid "Resend code"
msgstr "Code erneut senden"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "E-Mail erneut senden"
@@ -29939,6 +29941,11 @@ msgstr "TTS-Nachrichten senden"
msgid "Send verification code"
msgstr "Verifizierungscode senden"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Verifizierungs-E-Mail senden"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Beim Speichern dieser Einstellung ist ein Fehler aufgetreten. Bitte versuche es gleich erneut."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Beim Senden deiner Bestätigungs-E-Mail ist ein Fehler aufgetreten. Bitte versuche es gleich noch einmal."
@@ -32734,6 +32741,11 @@ msgstr "Untermenü"
msgid "Submit application"
msgstr "Antrag einreichen"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Feedback senden"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Wird gesendet"
@@ -37744,8 +37756,8 @@ msgstr "Bestätigungscode"
msgid "Verification code sent"
msgstr "Verifizierungscode gesendet"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Verifizierungs-E-Mail gesendet. Posteingang prüfen."
@@ -40176,7 +40188,7 @@ msgstr "Du bearbeitest Nachrichten zu schnell"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Du hast ein Geschenk erhalten"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Du hast zu viele Verifizierungs-E-Mails angefordert. Bitte warte einen Moment und versuche es erneut."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Δεν ήταν δυνατή η αποστολή του αιτήματος φιλίας. Δοκιμάστε ξανά."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Δεν ήταν δυνατή η αποστολή του email επαλήθευσης"
@@ -27910,8 +27910,10 @@ msgstr "Επανάληψη αποστολής ({secondsRemaining}δ)"
msgid "Resend code"
msgstr "Επανάληψη αποστολής κωδικού"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Επανάληψη αποστολής email"
@@ -29939,6 +29941,11 @@ msgstr "Αποστολή μηνυμάτων κειμένου σε ομιλία"
msgid "Send verification code"
msgstr "Αποστολή κωδικού επαλήθευσης"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Αποστολή email επαλήθευσης"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Κάτι πήγε στραβά κατά την αποθήκευση αυτής της ρύθμισης. Δοκιμάστε ξανά σε λίγο."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Κάτι πήγε στραβά κατά την αποστολή του email επαλήθευσης. Δοκιμάστε ξανά σε λίγο."
@@ -32734,6 +32741,11 @@ msgstr "Υπομενού"
msgid "Submit application"
msgstr "Υποβολή αίτησης"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Υποβολή σχολίων"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Υποβάλλεται"
@@ -37744,8 +37756,8 @@ msgstr "Κωδικός επαλήθευσης"
msgid "Verification code sent"
msgstr "Ο κωδικός επαλήθευσης στάλθηκε"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Στάλθηκε email επαλήθευσης. Ελέγξτε τα εισερχόμενά σας."
@@ -40176,7 +40188,7 @@ msgstr "Επεξεργάζεστε μηνύματα πολύ γρήγορα"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Λάβατε ένα δώρο"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Έχετε ζητήσει πάρα πολλά email επαλήθευσης. Περιμένετε λίγο και δοκιμάστε ξανά."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Couldn't send the friend request. Try again."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Couldn't send the verification email"
@@ -27910,8 +27910,10 @@ msgstr "Resend ({secondsRemaining}s)"
msgid "Resend code"
msgstr "Resend code"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Resend email"
@@ -29939,6 +29941,11 @@ msgstr "Send TTS messages"
msgid "Send verification code"
msgstr "Send verification code"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Send verification email"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Something went wrong while saving this setting. Please try again in a moment."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Something went wrong while sending your verification email. Please try again in a moment."
@@ -32734,6 +32741,11 @@ msgstr "Submenu"
msgid "Submit application"
msgstr "Submit application"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Submit feedback"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Submitting"
@@ -37744,8 +37756,8 @@ msgstr "Verification code"
msgid "Verification code sent"
msgstr "Verification code sent"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Verification email sent. Check your inbox."
@@ -40176,7 +40188,7 @@ msgstr "You're editing messages too quickly"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "You've received a gift"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "You've requested too many verification emails. Please wait a moment and try again."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Couldn't send the friend request. Try again."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Couldn't send the verification email"
@@ -27910,8 +27910,10 @@ msgstr "Resend ({secondsRemaining}s)"
msgid "Resend code"
msgstr "Resend code"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Resend email"
@@ -29939,6 +29941,11 @@ msgstr "Send TTS messages"
msgid "Send verification code"
msgstr "Send verification code"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Send verification email"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Something went wrong while saving this setting. Please try again in a moment."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Something went wrong while sending your verification email. Please try again in a moment."
@@ -32734,6 +32741,11 @@ msgstr "Submenu"
msgid "Submit application"
msgstr "Submit application"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Submit feedback"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Submitting"
@@ -37744,8 +37756,8 @@ msgstr "Verification code"
msgid "Verification code sent"
msgstr "Verification code sent"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Verification email sent. Check your inbox."
@@ -40176,7 +40188,7 @@ msgstr "You're editing messages too quickly"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "You've received a gift"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "You've requested too many verification emails. Please wait a moment and try again."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "No se pudo enviar la solicitud de amistad. Intenta de nuevo."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "No se pudo enviar el correo de verificación"
@@ -27910,8 +27910,10 @@ msgstr "Reenviar ({secondsRemaining} s)"
msgid "Resend code"
msgstr "Reenviar código"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Reenviar correo"
@@ -29939,6 +29941,11 @@ msgstr "Enviar mensajes con síntesis de voz"
msgid "Send verification code"
msgstr "Enviar código de verificación"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Enviar correo de verificación"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Algo salió mal al guardar esta configuración. Intenta de nuevo en un momento."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Algo salió mal al enviar tu correo electrónico de verificación. Intenta de nuevo en un momento."
@@ -32734,6 +32741,11 @@ msgstr "Submenú"
msgid "Submit application"
msgstr "Enviar solicitud"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Enviar comentarios"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Enviando"
@@ -37744,8 +37756,8 @@ msgstr "Código de verificación"
msgid "Verification code sent"
msgstr "Código de verificación enviado"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Correo electrónico de verificación enviado. Revisa tu bandeja de entrada."
@@ -40176,7 +40188,7 @@ msgstr "Estás editando mensajes muy rápido"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Recibiste un regalo"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Has solicitado demasiados correos de verificación. Espera un momento y vuelve a intentarlo."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "No se pudo enviar la solicitud de amistad. Inténtalo de nuevo."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "No se pudo enviar el correo de verificación"
@@ -27910,8 +27910,10 @@ msgstr "Reenviar ({secondsRemaining} s)"
msgid "Resend code"
msgstr "Reenviar código"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Reenviar correo"
@@ -29939,6 +29941,11 @@ msgstr "Enviar mensajes con síntesis de voz"
msgid "Send verification code"
msgstr "Enviar código de verificación"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Enviar correo de verificación"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Algo salió mal al guardar este ajuste. Por favor, inténtalo de nuevo en un momento."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Algo salió mal al enviar tu correo de verificación. Por favor, inténtalo de nuevo en un momento."
@@ -32734,6 +32741,11 @@ msgstr "Submenú"
msgid "Submit application"
msgstr "Enviar solicitud"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Enviar comentarios"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Enviando"
@@ -37744,8 +37756,8 @@ msgstr "Código de verificación"
msgid "Verification code sent"
msgstr "Código de verificación enviado"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Correo electrónico de verificación enviado. Revisa tu bandeja de entrada."
@@ -40176,7 +40188,7 @@ msgstr "Estás editando mensajes demasiado rápido"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Has recibido un regalo"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Has solicitado demasiados correos de verificación. Espera un momento y vuelve a intentarlo."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Kaveripyyntöä ei voitu lähettää. Yritä uudelleen."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Vahvistussähköpostin lähetys epäonnistui"
@@ -27910,8 +27910,10 @@ msgstr "Lähetä uudelleen ({secondsRemaining} s)"
msgid "Resend code"
msgstr "Lähetä koodi uudelleen"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Lähetä sähköposti uudelleen"
@@ -29939,6 +29941,11 @@ msgstr "Lähetä tekstistä puheeksi -viestejä"
msgid "Send verification code"
msgstr "Lähetä vahvistuskoodi"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Lähetä vahvistussähköposti"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Jokin meni pieleen asetusta tallennettaessa. Yritä uudelleen hetken kuluttua."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Jokin meni pieleen vahvistussähköpostia lähetettäessä. Yritä uudelleen hetken kuluttua."
@@ -32734,6 +32741,11 @@ msgstr "Alivalikko"
msgid "Submit application"
msgstr "Lähetä hakemus"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Lähetä palautetta"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Lähetetään"
@@ -37744,8 +37756,8 @@ msgstr "Vahvistuskoodi"
msgid "Verification code sent"
msgstr "Vahvistuskoodi lähetetty"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Vahvistussähköposti lähetetty. Tarkista saapuneet-kansiosi."
@@ -40176,7 +40188,7 @@ msgstr "Muokkaat viestejä liian nopeasti"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Olet saanut lahjan"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Olet pyytänyt liian monta vahvistussähköpostia. Odota hetki ja yritä uudelleen."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Impossible d'envoyer la demande d'ami. Veuillez réessayer."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Impossible d’envoyer l’e-mail de vérification"
@@ -27910,8 +27910,10 @@ msgstr "Renvoyer ({secondsRemaining} s)"
msgid "Resend code"
msgstr "Renvoyer le code"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Renvoyer l'e-mail"
@@ -29939,6 +29941,11 @@ msgstr "Envoyer des messages de synthèse vocale"
msgid "Send verification code"
msgstr "Envoyer le code de vérification"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Envoyer l'e-mail de vérification"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Un problème est survenu lors de l’enregistrement de ce paramètre. Veuillez réessayer dans un instant."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Un problème est survenu lors de l'envoi de votre e-mail de vérification. Veuillez réessayer dans un instant."
@@ -32734,6 +32741,11 @@ msgstr "Sous-menu"
msgid "Submit application"
msgstr "Envoyer la candidature"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Envoyer un commentaire"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Envoi en cours"
@@ -37744,8 +37756,8 @@ msgstr "Code de vérification"
msgid "Verification code sent"
msgstr "Code de vérification envoyé"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "E-mail de vérification envoyé. Vérifiez votre boîte de réception."
@@ -40176,7 +40188,7 @@ msgstr "Vous modifiez les messages trop rapidement"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Vous avez reçu un cadeau"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Vous avez demandé trop d’e-mails de vérification. Veuillez patienter un instant et réessayer."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "לא ניתן לשלוח את בקשת החברות. יש לנסות שוב."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "לא ניתן היה לשלוח את אימייל האימות"
@@ -27910,8 +27910,10 @@ msgstr "שלח שוב ({secondsRemaining} שנ')"
msgid "Resend code"
msgstr "שלח קוד שוב"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "שלח אימייל שוב"
@@ -29939,6 +29941,11 @@ msgstr "שליחת הודעות טקסט לדיבור"
msgid "Send verification code"
msgstr "שלח קוד אימות"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "שלח אימייל אימות"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "משהו השתבש במהלך שמירת ההגדרה הזו. יש לנסות שוב בעוד רגע."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "משהו השתבש במהלך שליחת אימייל האימות שלך. יש לנסות שוב בעוד רגע."
@@ -32734,6 +32741,11 @@ msgstr "תפריט משנה"
msgid "Submit application"
msgstr "שליחת בקשה"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "שלח משוב"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "שולח"
@@ -37744,8 +37756,8 @@ msgstr "קוד אימות"
msgid "Verification code sent"
msgstr "קוד האימות נשלח"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "נשלחה הודעת אימות. יש לבדוק את תיבת הדואר הנכנס."
@@ -40176,7 +40188,7 @@ msgstr "אתה עורך הודעות מהר מדי"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "קיבלת מתנה"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "ביקשת יותר מדי אימיילי אימות. המתן רגע ונסה שוב."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "फ्रेंड रिक्वेस्ट नहीं भेजी जा सकी। फिर से कोशिश करें।"
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "पुष्टि करने वाला ईमेल भेजा नहीं जा सका"
@@ -27910,8 +27910,10 @@ msgstr "फिर से भेजें ({secondsRemaining}से)"
msgid "Resend code"
msgstr "कोड फिर से भेजें"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "ईमेल फिर से भेजें"
@@ -29939,6 +29941,11 @@ msgstr "TTS मैसेज भेजें"
msgid "Send verification code"
msgstr "वेरिफ़िकेशन कोड भेजें"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "सत्यापन ईमेल भेजें"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "इस सेटिंग को सेव करते समय कुछ गड़बड़ हो गई। कृपया कुछ देर बाद फिर से कोशिश करें।"
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "आपका वेरिफ़िकेशन ईमेल भेजने में कुछ गड़बड़ हो गई। कृपया कुछ देर बाद फिर से कोशिश करें।"
@@ -32734,6 +32741,11 @@ msgstr "सबमेनू"
msgid "Submit application"
msgstr "आवेदन जमा करें"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "फ़ीडबैक सबमिट करें"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "सबमिट किया जा रहा है"
@@ -37744,8 +37756,8 @@ msgstr "वेरिफ़िकेशन कोड"
msgid "Verification code sent"
msgstr "वेरिफ़िकेशन कोड भेजा गया"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "वेरिफ़िकेशन ईमेल भेजा गया है। अपना इनबॉक्स देखें।"
@@ -40176,7 +40188,7 @@ msgstr "आप बहुत तेज़ी से मैसेज एडिट
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "आपको एक उपहार मिला है"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "आपने बहुत ज़्यादा वेरिफ़िकेशन ईमेल का अनुरोध किया है। कृपया कुछ देर इंतज़ार करें और फिर से कोशिश करें।"
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Nije moguće poslati zahtjev za prijateljstvo. Pokušajte ponovno."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Nije moguće poslati e-poštu za potvrdu"
@@ -27910,8 +27910,10 @@ msgstr "Ponovno pošalji ({secondsRemaining}s)"
msgid "Resend code"
msgstr "Ponovno pošalji kod"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Ponovno pošalji e-poštu"
@@ -29939,6 +29941,11 @@ msgstr "Slanje TTS poruka"
msgid "Send verification code"
msgstr "Pošalji kod za provjeru"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Pošalji e-poruku za provjeru"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Došlo je do pogreške prilikom spremanja ove postavke. Pokušajte ponovno za nekoliko trenutaka."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Nešto je pošlo po zlu prilikom slanja e-pošte za potvrdu. Pokušajte ponovno za nekoliko trenutaka."
@@ -32734,6 +32741,11 @@ msgstr "Podizbornik"
msgid "Submit application"
msgstr "Pošalji prijavu"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Pošalji povratne informacije"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Slanje u tijeku"
@@ -37744,8 +37756,8 @@ msgstr "Kod za provjeru"
msgid "Verification code sent"
msgstr "Kod za provjeru poslan"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "E-pošta za potvrdu poslana. Provjerite svoju pristiglu poštu."
@@ -40176,7 +40188,7 @@ msgstr "Prebrzo uređujete poruke"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Primili ste poklon"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Previše ste puta zatražili e-poštu za potvrdu. Pričekajte trenutak pa pokušajte ponovno."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Nem sikerült elküldeni az ismerősjelölést. Próbáld újra."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Nem sikerült elküldeni az ellenőrző e-mailt"
@@ -27910,8 +27910,10 @@ msgstr "Újraküldés ({secondsRemaining} mp)"
msgid "Resend code"
msgstr "Kód újraküldése"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "E-mail újraküldése"
@@ -29939,6 +29941,11 @@ msgstr "TTS-üzenetek küldése"
msgid "Send verification code"
msgstr "Ellenőrző kód küldése"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Küldj egy ellenőrző e-mailt"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Hiba történt a beállítás mentésekor. Próbáld újra egy kis idő múlva."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Hiba történt a megerősítő e-mail elküldésekor. Próbáld újra egy kis idő múlva."
@@ -32734,6 +32741,11 @@ msgstr "Almenü"
msgid "Submit application"
msgstr "Jelentkezés beküldése"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Visszajelzés küldése"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Küldés folyamatban"
@@ -37744,8 +37756,8 @@ msgstr "Ellenőrző kód"
msgid "Verification code sent"
msgstr "Ellenőrző kód elküldve"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Ellenőrző e-mail elküldve. Nézd meg a beérkező üzeneteidet."
@@ -40176,7 +40188,7 @@ msgstr "Túl gyorsan szerkeszted az üzeneteket"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Ajándékot kaptál"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Túl sok ellenőrző e-mailt kértél. Kérjük, várj egy kicsit, majd próbáld újra."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Tidak dapat mengirim permintaan pertemanan. Coba lagi."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Tidak dapat mengirim email verifikasi"
@@ -27910,8 +27910,10 @@ msgstr "Kirim ulang ({secondsRemaining} dtk)"
msgid "Resend code"
msgstr "Kirim ulang kode"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Kirim ulang email"
@@ -29939,6 +29941,11 @@ msgstr "Kirim pesan TTS"
msgid "Send verification code"
msgstr "Kirim kode verifikasi"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Kirim email verifikasi"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Terjadi kesalahan saat menyimpan pengaturan ini. Silakan coba lagi sebentar lagi."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Terjadi kesalahan saat mengirim email verifikasimu. Silakan coba lagi sebentar lagi."
@@ -32734,6 +32741,11 @@ msgstr "Submenu"
msgid "Submit application"
msgstr "Kirim pengajuan"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Kirim masukan"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Mengirim"
@@ -37744,8 +37756,8 @@ msgstr "Kode verifikasi"
msgid "Verification code sent"
msgstr "Kode verifikasi terkirim"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Email verifikasi telah dikirim. Periksa kotak masukmu."
@@ -40176,7 +40188,7 @@ msgstr "Kamu mengedit pesan terlalu cepat"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Kamu menerima hadiah"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Kamu sudah terlalu banyak meminta email verifikasi. Tunggu sebentar lalu coba lagi."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Impossibile inviare la richiesta di amicizia. Riprova."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Impossibile inviare l'email di verifica"
@@ -27910,8 +27910,10 @@ msgstr "Invia di nuovo ({secondsRemaining}s)"
msgid "Resend code"
msgstr "Reinvia codice"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Invia di nuovo l'email"
@@ -29939,6 +29941,11 @@ msgstr "Invia messaggi TTS"
msgid "Send verification code"
msgstr "Invia codice di verifica"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Invia email di verifica"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Si è verificato un errore durante il salvataggio di questa impostazione. Riprova tra qualche istante."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Si è verificato un errore durante l'invio dell'email di verifica. Riprova tra qualche istante."
@@ -32734,6 +32741,11 @@ msgstr "Sottomenu"
msgid "Submit application"
msgstr "Invia candidatura"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Invia feedback"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Invio in corso"
@@ -37744,8 +37756,8 @@ msgstr "Codice di verifica"
msgid "Verification code sent"
msgstr "Codice di verifica inviato"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Email di verifica inviata. Controlla la tua casella di posta."
@@ -40176,7 +40188,7 @@ msgstr "Stai modificando i messaggi troppo velocemente"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Hai ricevuto un regalo"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Hai richiesto troppe email di verifica. Attendi un momento e riprova."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "友達リクエストを送信できませんでした。もう一度お試しください。"
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "確認メールを送信できませんでした"
@@ -27910,8 +27910,10 @@ msgstr "再送({secondsRemaining}秒)"
msgid "Resend code"
msgstr "コードを再送"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "メールを再送"
@@ -29939,6 +29941,11 @@ msgstr "TTSメッセージを送信"
msgid "Send verification code"
msgstr "認証コードを送信"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "確認メールを送信"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "設定の保存中に問題が発生しました。しばらくしてからもう一度お試しください。"
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "確認メールの送信中に問題が発生しました。しばらくしてからもう一度お試しください。"
@@ -32734,6 +32741,11 @@ msgstr "サブメニュー"
msgid "Submit application"
msgstr "申請を送信"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "フィードバックを送信"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "送信中"
@@ -37744,8 +37756,8 @@ msgstr "認証コード"
msgid "Verification code sent"
msgstr "認証コードを送信しました"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "確認メールを送信しました。受信トレイをご確認ください。"
@@ -40176,7 +40188,7 @@ msgstr "メッセージの編集ペースが速すぎます"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "ギフトが届きました"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "確認メールの送信回数が多すぎます。しばらく待ってからもう一度お試しください。"
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "친구 요청을 보낼 수 없습니다. 다시 시도해 주세요."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "인증 이메일을 보낼 수 없습니다"
@@ -27910,8 +27910,10 @@ msgstr "재전송 ({secondsRemaining}초)"
msgid "Resend code"
msgstr "코드 재전송"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "이메일 재전송"
@@ -29939,6 +29941,11 @@ msgstr "TTS 메시지 보내기"
msgid "Send verification code"
msgstr "인증 코드 전송"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "인증 이메일 보내기"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "설정 저장 중 문제가 발생했습니다. 잠시 후 다시 시도해 주세요."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "인증 이메일을 보내는 중 문제가 발생했습니다. 잠시 후 다시 시도해 주세요."
@@ -32734,6 +32741,11 @@ msgstr "하위 메뉴"
msgid "Submit application"
msgstr "신청서 제출"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "피드백 제출"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "제출 중"
@@ -37744,8 +37756,8 @@ msgstr "인증 코드"
msgid "Verification code sent"
msgstr "인증 코드 전송됨"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "인증 이메일을 보냈습니다. 받은편지함을 확인해 주세요."
@@ -40176,7 +40188,7 @@ msgstr "메시지를 너무 빨리 수정하고 있습니다"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "선물을 받았어요"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "인증 이메일을 너무 많이 요청했습니다. 잠시 기다렸다가 다시 시도해 주세요."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Nepavyko išsiųsti draugystės užklausos. Bandykite dar kartą."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Nepavyko išsiųsti patvirtinimo el. laiško"
@@ -27911,8 +27911,10 @@ msgstr "Siųsti iš naujo ({secondsRemaining} s)"
msgid "Resend code"
msgstr "Siųsti kodą iš naujo"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Iš naujo siųsti el. laišką"
@@ -29940,6 +29942,11 @@ msgstr "Siųsti TTS žinutes"
msgid "Send verification code"
msgstr "Siųsti patvirtinimo kodą"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Siųsti patvirtinimo el. laišką"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31696,7 +31703,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Nepavyko išsaugoti šio nustatymo. Bandykite dar kartą po akimirkos."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Nepavyko išsiųsti patvirtinimo el. laiško. Bandykite dar kartą po akimirkos."
@@ -32735,6 +32742,11 @@ msgstr "Antrinis meniu"
msgid "Submit application"
msgstr "Pateikti paraišką"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Pateikti atsiliepimą"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Pateikiama"
@@ -37745,8 +37757,8 @@ msgstr "Patvirtinimo kodas"
msgid "Verification code sent"
msgstr "Patvirtinimo kodas išsiųstas"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Patvirtinimo el. laiškas išsiųstas. Patikrinkite gautuosius."
@@ -40177,7 +40189,7 @@ msgstr "Per greitai redaguojate žinutes"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40341,7 +40353,7 @@ msgid "You've received a gift"
msgstr "Gavote dovaną"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Išsiuntėte per daug patvirtinimo el. laiškų. Palaukite ir bandykite dar kartą."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Vriendschapsverzoek kon niet worden verzonden. Probeer het opnieuw."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Verificatie-e-mail kon niet worden verzonden"
@@ -27910,8 +27910,10 @@ msgstr "Opnieuw verzenden ({secondsRemaining} s)"
msgid "Resend code"
msgstr "Code opnieuw verzenden"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "E-mail opnieuw verzenden"
@@ -29939,6 +29941,11 @@ msgstr "TTS-berichten verzenden"
msgid "Send verification code"
msgstr "Verificatiecode verzenden"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Verificatiemail verzenden"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Er is iets misgegaan bij het opslaan van deze instelling. Probeer het over een ogenblik opnieuw."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Er is iets misgegaan bij het verzenden van je verificatie-e-mail. Probeer het over een ogenblik opnieuw."
@@ -32734,6 +32741,11 @@ msgstr "Submenu"
msgid "Submit application"
msgstr "Aanvraag indienen"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Feedback indienen"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Verzenden"
@@ -37744,8 +37756,8 @@ msgstr "Verificatiecode"
msgid "Verification code sent"
msgstr "Verificatiecode verzonden"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Verificatie-e-mail verzonden. Controleer je inbox."
@@ -40176,7 +40188,7 @@ msgstr "Je bewerkt berichten te snel"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Je hebt een cadeau ontvangen"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Je hebt te veel verificatie-e-mails aangevraagd. Wacht even en probeer het opnieuw."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Kunne ikke sende venneforespørselen. Prøv igjen."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Kunne ikke sende bekreftelseseposten"
@@ -27910,8 +27910,10 @@ msgstr "Send på nytt ({secondsRemaining} s)"
msgid "Resend code"
msgstr "Send kode på nytt"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Send e-post på nytt"
@@ -29939,6 +29941,11 @@ msgstr "Send tekst-til-tale-meldinger"
msgid "Send verification code"
msgstr "Send bekreftelseskode"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Send bekreftelsesepost"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Noe gikk galt under lagring av denne innstillingen. Prøv igjen om en liten stund."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Noe gikk galt under sending av bekreftelses-e-posten din. Prøv igjen om et øyeblikk."
@@ -32734,6 +32741,11 @@ msgstr "Undermeny"
msgid "Submit application"
msgstr "Send søknad"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Send tilbakemelding"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Sender inn"
@@ -37744,8 +37756,8 @@ msgstr "Bekreftelseskode"
msgid "Verification code sent"
msgstr "Bekreftelseskode sendt"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "E-post for bekreftelse er sendt. Sjekk innboksen din."
@@ -40176,7 +40188,7 @@ msgstr "Du redigerer meldinger for raskt"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Du har mottatt en gave"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Du har bedt om for mange bekreftelses-e-poster. Vent litt og prøv igjen."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Nie udało się wysłać zaproszenia do znajomych. Spróbuj ponownie."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Nie udało się wysłać e-maila weryfikacyjnego"
@@ -27910,8 +27910,10 @@ msgstr "Wyślij ponownie ({secondsRemaining} s)"
msgid "Resend code"
msgstr "Wyślij kod ponownie"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Wyślij e-mail ponownie"
@@ -29939,6 +29941,11 @@ msgstr "Wysyłanie wiadomości TTS"
msgid "Send verification code"
msgstr "Wyślij kod weryfikacyjny"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Wyślij e-mail weryfikacyjny"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Coś poszło nie tak podczas zapisywania tego ustawienia. Spróbuj ponownie za chwilę."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Coś poszło nie tak podczas wysyłania e-maila weryfikacyjnego. Spróbuj ponownie za chwilę."
@@ -32734,6 +32741,11 @@ msgstr "Podmenu"
msgid "Submit application"
msgstr "Wyślij zgłoszenie"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Prześlij opinię"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Wysyłanie"
@@ -37744,8 +37756,8 @@ msgstr "Kod weryfikacyjny"
msgid "Verification code sent"
msgstr "Kod weryfikacyjny wysłany"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Wysłano e-mail weryfikacyjny. Sprawdź swoją skrzynkę odbiorczą."
@@ -40176,7 +40188,7 @@ msgstr "Zbyt szybko edytujesz wiadomości"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Otrzymano prezent"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Wysłano zbyt wiele próśb o e-maile weryfikacyjne. Poczekaj chwilę i spróbuj ponownie."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Não foi possível enviar o pedido de amizade. Tente novamente."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Não foi possível enviar o e-mail de verificação"
@@ -27910,8 +27910,10 @@ msgstr "Reenviar ({secondsRemaining}s)"
msgid "Resend code"
msgstr "Reenviar código"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Reenviar e-mail"
@@ -29939,6 +29941,11 @@ msgstr "Enviar mensagens TTS"
msgid "Send verification code"
msgstr "Enviar código de verificação"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Enviar e-mail de verificação"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Ocorreu um erro ao salvar esta configuração. Tente novamente em alguns instantes."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Algo deu errado ao enviar seu e-mail de verificação. Tente novamente em alguns instantes."
@@ -32734,6 +32741,11 @@ msgstr "Submenu"
msgid "Submit application"
msgstr "Enviar candidatura"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Enviar feedback"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Enviando"
@@ -37744,8 +37756,8 @@ msgstr "Código de verificação"
msgid "Verification code sent"
msgstr "Código de verificação enviado"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "E-mail de verificação enviado. Verifique sua caixa de entrada."
@@ -40176,7 +40188,7 @@ msgstr "Você está editando mensagens muito rápido"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Você recebeu um presente"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Você solicitou muitos e-mails de verificação. Aguarde um momento e tente novamente."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Nu s-a putut trimite cererea de prietenie. Încearcă din nou."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Nu s-a putut trimite e-mailul de verificare"
@@ -27910,8 +27910,10 @@ msgstr "Retrimite ({secondsRemaining}s)"
msgid "Resend code"
msgstr "Retrimite codul"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Retrimite e-mailul"
@@ -29939,6 +29941,11 @@ msgstr "Trimite mesaje TTS"
msgid "Send verification code"
msgstr "Trimite codul de verificare"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Trimite email de verificare"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "A apărut o eroare la salvarea acestei setări. Te rugăm să încerci din nou mai târziu."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "A apărut o eroare la trimiterea e-mailului de verificare. Te rugăm să încerci din nou mai târziu."
@@ -32734,6 +32741,11 @@ msgstr "Submeniu"
msgid "Submit application"
msgstr "Trimite cererea"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Trimite feedback"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Se trimite"
@@ -37744,8 +37756,8 @@ msgstr "Cod de verificare"
msgid "Verification code sent"
msgstr "Codul de verificare a fost trimis"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "E-mailul de verificare a fost trimis. Verifică-ți căsuța de e-mail."
@@ -40176,7 +40188,7 @@ msgstr "Editezi mesajele prea repede"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Ai primit un cadou"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Ai solicitat prea multe e-mailuri de verificare. Așteaptă un moment și încearcă din nou."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Не удалось отправить запрос на добавление в друзья. Повторите попытку."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Не удалось отправить письмо для подтверждения"
@@ -27910,8 +27910,10 @@ msgstr "Отправить повторно ({secondsRemaining} с)"
msgid "Resend code"
msgstr "Отправить код повторно"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Отправить письмо повторно"
@@ -29939,6 +29941,11 @@ msgstr "Отправлять сообщения TTS"
msgid "Send verification code"
msgstr "Отправить код подтверждения"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Отправить письмо для подтверждения"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Что-то пошло не так при сохранении этой настройки. Повторите попытку через минуту."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Что-то пошло не так при отправке письма для подтверждения. Повторите попытку через минуту."
@@ -32734,6 +32741,11 @@ msgstr "Подменю"
msgid "Submit application"
msgstr "Отправить заявку"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Отправить отзыв"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Отправка"
@@ -37744,8 +37756,8 @@ msgstr "Код подтверждения"
msgid "Verification code sent"
msgstr "Код подтверждения отправлен"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Письмо для подтверждения отправлено. Проверьте входящие."
@@ -40176,7 +40188,7 @@ msgstr "Вы слишком быстро редактируете сообщен
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Вы получили подарок"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Вы запросили слишком много писем с подтверждением. Подождите немного и попробуйте снова."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Kunde inte skicka vänförfrågan. Försök igen."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Kunde inte skicka verifieringsmeddelandet"
@@ -27910,8 +27910,10 @@ msgstr "Skicka igen ({secondsRemaining} s)"
msgid "Resend code"
msgstr "Skicka kod igen"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Skicka e-post igen"
@@ -29939,6 +29941,11 @@ msgstr "Skicka TTS-meddelanden"
msgid "Send verification code"
msgstr "Skicka verifieringskod"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Skicka verifieringsmejl"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Något gick fel när inställningen skulle sparas. Försök igen om en liten stund."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Något gick fel när vi skulle skicka ditt verifieringsmejl. Försök igen om en liten stund."
@@ -32734,6 +32741,11 @@ msgstr "Undermeny"
msgid "Submit application"
msgstr "Skicka ansökan"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Skicka feedback"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Skickar"
@@ -37744,8 +37756,8 @@ msgstr "Verifieringskod"
msgid "Verification code sent"
msgstr "Verifieringskod skickad"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Verifieringsmejl skickat. Kolla din inkorg."
@@ -40176,7 +40188,7 @@ msgstr "Du redigerar meddelanden för snabbt"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Du har fått en present"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Du har begärt för många verifieringsmejl. Vänta en stund och försök igen."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "ส่งคำขอเป็นเพื่อนไม่สำเร็จ ลองอีกครั้ง"
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "ส่งอีเมลยืนยันไม่ได้"
@@ -27910,8 +27910,10 @@ msgstr "ส่งอีกครั้ง ({secondsRemaining} วินาที
msgid "Resend code"
msgstr "ส่งรหัสอีกครั้ง"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "ส่งอีเมลอีกครั้ง"
@@ -29939,6 +29941,11 @@ msgstr "ส่งข้อความ TTS"
msgid "Send verification code"
msgstr "ส่งรหัสยืนยัน"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "ส่งอีเมลยืนยัน"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "เกิดข้อผิดพลาดขณะบันทึกการตั้งค่านี้ โปรดลองอีกครั้งในภายหลัง"
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "เกิดข้อผิดพลาดขณะส่งอีเมลยืนยันของคุณ โปรดลองอีกครั้งในภายหลัง"
@@ -32734,6 +32741,11 @@ msgstr "เมนูย่อย"
msgid "Submit application"
msgstr "ส่งใบสมัคร"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "ส่งคำติชม"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "กำลังส่ง"
@@ -37744,8 +37756,8 @@ msgstr "รหัสยืนยัน"
msgid "Verification code sent"
msgstr "ส่งรหัสยืนยันแล้ว"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "ส่งอีเมลยืนยันแล้ว โปรดตรวจสอบกล่องจดหมายเข้าของคุณ"
@@ -40176,7 +40188,7 @@ msgstr "คุณแก้ไขข้อความเร็วเกินไ
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "คุณได้รับของขวัญ"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "คุณส่งคำขออีเมลยืนยันมากเกินไปแล้ว โปรดรอสักครู่แล้วลองอีกครั้ง"
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Arkadaşlık isteği gönderilemedi. Tekrar deneyin."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Doğrulama e-postası gönderilemedi"
@@ -27910,8 +27910,10 @@ msgstr "Tekrar gönder ({secondsRemaining}sn)"
msgid "Resend code"
msgstr "Kodu tekrar gönder"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "E-postayı tekrar gönder"
@@ -29939,6 +29941,11 @@ msgstr "TTS mesajları gönder"
msgid "Send verification code"
msgstr "Doğrulama kodu gönder"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Doğrulama e-postası gönder"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Bu ayarı kaydederken bir sorun oluştu. Lütfen bir süre sonra tekrar deneyin."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Doğrulama e-postanızı gönderirken bir sorun oluştu. Lütfen bir süre sonra tekrar deneyin."
@@ -32734,6 +32741,11 @@ msgstr "Alt menü"
msgid "Submit application"
msgstr "Başvuruyu gönder"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Geri bildirim gönder"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Gönderiliyor"
@@ -37744,8 +37756,8 @@ msgstr "Doğrulama kodu"
msgid "Verification code sent"
msgstr "Doğrulama kodu gönderildi"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Doğrulama e-postası gönderildi. Gelen kutunuzu kontrol edin."
@@ -40176,7 +40188,7 @@ msgstr "Mesajları çok hızlı düzenliyorsunuz"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Bir hediye aldınız"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Çok fazla doğrulama e-postası istediniz. Lütfen bir süre bekleyip tekrar deneyin."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Не вдалося надіслати запит на додавання в друзі. Спробуйте ще раз."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Не вдалося надіслати лист для підтвердження"
@@ -27910,8 +27910,10 @@ msgstr "Надіслати повторно ({secondsRemaining} с)"
msgid "Resend code"
msgstr "Надіслати код повторно"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Надіслати лист повторно"
@@ -29939,6 +29941,11 @@ msgstr "Надсилати TTS-повідомлення"
msgid "Send verification code"
msgstr "Надіслати код підтвердження"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Надіслати лист для підтвердження"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Під час збереження цього параметра сталася помилка. Спробуйте ще раз за мить."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Під час надсилання листа для підтвердження сталася помилка. Спробуйте ще раз за мить."
@@ -32734,6 +32741,11 @@ msgstr "Підменю"
msgid "Submit application"
msgstr "Надіслати заявку"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Надіслати відгук"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Надсилання"
@@ -37744,8 +37756,8 @@ msgstr "Код підтвердження"
msgid "Verification code sent"
msgstr "Код підтвердження надіслано"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Лист із підтвердженням надіслано. Перевірте свою поштову скриньку."
@@ -40176,7 +40188,7 @@ msgstr "Ви надто швидко редагуєте повідомлення
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Ви отримали подарунок"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Ви надіслали забагато листів для підтвердження. Зачекайте трохи й спробуйте знову."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "Không gửi được lời mời kết bạn. Vui lòng thử lại."
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "Không thể gửi email xác minh"
@@ -27910,8 +27910,10 @@ msgstr "Gửi lại ({secondsRemaining}s)"
msgid "Resend code"
msgstr "Gửi lại mã"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "Gửi lại email"
@@ -29939,6 +29941,11 @@ msgstr "Gửi tin nhắn TTS"
msgid "Send verification code"
msgstr "Gửi mã xác minh"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "Gửi email xác minh"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "Đã xảy ra lỗi khi lưu cài đặt này. Vui lòng thử lại sau giây lát."
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "Đã xảy ra lỗi khi gửi email xác minh của bạn. Vui lòng thử lại sau giây lát."
@@ -32734,6 +32741,11 @@ msgstr "Menu phụ"
msgid "Submit application"
msgstr "Gửi đơn đăng ký"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "Gửi phản hồi"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "Đang gửi"
@@ -37744,8 +37756,8 @@ msgstr "Mã xác minh"
msgid "Verification code sent"
msgstr "Đã gửi mã xác minh"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "Đã gửi email xác minh. Kiểm tra hộp thư đến của bạn."
@@ -40176,7 +40188,7 @@ msgstr "Bạn đang chỉnh sửa tin nhắn quá nhanh"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "Bạn đã nhận được quà"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "Bạn đã yêu cầu quá nhiều email xác minh. Vui lòng đợi một lát rồi thử lại."
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "无法发送好友请求。请重试。"
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "无法发送验证邮件"
@@ -27910,8 +27910,10 @@ msgstr "重新发送({secondsRemaining} 秒)"
msgid "Resend code"
msgstr "重新发送验证码"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "重新发送邮件"
@@ -29939,6 +29941,11 @@ msgstr "发送 TTS 消息"
msgid "Send verification code"
msgstr "发送验证码"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "发送验证邮件"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "保存此设置时出错了。请稍后重试。"
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "发送验证邮件时出错了。请稍后重试。"
@@ -32734,6 +32741,11 @@ msgstr "子菜单"
msgid "Submit application"
msgstr "提交申请"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "提交反馈"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "正在提交"
@@ -37744,8 +37756,8 @@ msgstr "验证码"
msgid "Verification code sent"
msgstr "验证码已发送"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "验证邮件已发送。请检查你的收件箱。"
@@ -40176,7 +40188,7 @@ msgstr "你编辑消息的速度太快了"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "你收到了一份礼物"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "你请求的验证邮件过多。请稍候片刻,然后重试。"
+18 -6
View File
@@ -10451,7 +10451,7 @@ msgid "Couldn't send the friend request. Try again."
msgstr "無法傳送好友邀請。請再試一次。"
#. Title of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Couldn't send the verification email"
msgstr "無法傳送驗證電子郵件"
@@ -27910,8 +27910,10 @@ msgstr "重新傳送({secondsRemaining} 秒)"
msgid "Resend code"
msgstr "重新傳送驗證碼"
#. Button label that sends another verification email after one was already sent. Keep it concise.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/flow/IpAuthorizationScreen.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Resend email"
msgstr "重寄驗證信"
@@ -29939,6 +29941,11 @@ msgstr "傳送 TTS 訊息"
msgid "Send verification code"
msgstr "傳送驗證碼"
#. Button label on prompts that ask the user to verify their email, such as the add friend form. Sends a verification email right away. Keep it concise.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Send verification email"
msgstr "寄送驗證信件"
#. Accessible label and tooltip for the send button after recording a voice message.
#. Keyboard shortcut action label that opens the desktop voice message composer.
#. Plus menu item that opens the desktop voice message composer to record and send a voice clip.
@@ -31695,7 +31702,7 @@ msgid "Something went wrong while saving this setting. Please try again in a mom
msgstr "儲存此設定時發生錯誤。請稍後再試一次。"
#. Body of the error modal shown when sending the verification email fails unexpectedly.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Something went wrong while sending your verification email. Please try again in a moment."
msgstr "傳送驗證電子郵件時發生錯誤。請稍後再試一次。"
@@ -32734,6 +32741,11 @@ msgstr "子選單"
msgid "Submit application"
msgstr "送出申請"
#. Settings menu entry that opens the feedback website, where people suggest features and report problems.
#: src/features/i18n/utils/CommonMessageDescriptors.ts
msgid "Submit feedback"
msgstr "提交意見回饋"
#: src/features/user/components/modals/tabs/component_gallery_tab/ComponentGalleryTabButtonsTab.tsx
msgid "Submitting"
msgstr "正在送出"
@@ -37744,8 +37756,8 @@ msgstr "驗證碼"
msgid "Verification code sent"
msgstr "驗證碼已送出"
#. Toast confirmation in the email verification alert after a verification email has been sent.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#. Toast confirmation shown after a verification email has been sent.
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "Verification email sent. Check your inbox."
msgstr "驗證電子郵件已寄出。請檢查您的收件匣。"
@@ -40176,7 +40188,7 @@ msgstr "您編輯訊息的速度太快了"
#. Title of the error modal shown when resending a verification email is rate-limited.
#. Title of the error modal shown when sharing or sending a theme link is rate limited.
#: src/features/app/components/alerts/CommonErrorModalDescriptors.ts
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
#: src/features/discovery/commands/DiscoveryJoinCommands.tsx
#: src/features/emoji/components/emojis/EmojiListItem.tsx
#: src/features/theme/components/modals/ShareThemeModal.tsx
@@ -40340,7 +40352,7 @@ msgid "You've received a gift"
msgstr "您收到一份禮物"
#. Body of the error modal shown when resending a verification email is rate-limited.
#: src/features/app/components/dialogs/components/EmailVerificationAlert.tsx
#: src/features/auth/hooks/useVerifyEmailAction.tsx
msgid "You've requested too many verification emails. Please wait a moment and try again."
msgstr "您已要求傳送太多驗證電子郵件。請稍候片刻再試一次。"
@@ -614,6 +614,10 @@ export const WHATS_NEW_DESCRIPTOR = msg({
message: "What's new",
comment: 'Settings menu entry that opens the latest patch notes for the app on the website.',
});
export const SUBMIT_FEEDBACK_DESCRIPTOR = msg({
message: 'Submit feedback',
comment: 'Settings menu entry that opens the feedback website, where people suggest features and report problems.',
});
export const OPENS_IN_BROWSER_DESCRIPTOR = msg({
message: 'Opens in your browser',
comment:
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {IconProps} from '@phosphor-icons/react';
import React from 'react';
export const ForumChannelIcon = React.forwardRef<SVGSVGElement, IconProps>(({size = 256, className, ...props}, ref) => {
return (
<svg
ref={ref}
width={size}
height={size}
viewBox="0 0 256 256"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-hidden={true}
data-flx="ui.icons.locked-announcement-channel-icon.svg"
{...props}
>
<path
fill="currentColor"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
d="M169.57,72.59A80,80,0,0,0,16,104v64a16,16,0,0,0,16,16H86.67A80.15,80.15,0,0,0,160,232h64a16,16,0,0,0,16-16V152A80,80,0,0,0,169.57,72.59ZM224,216H160a64.14,64.14,0,0,1-55.68-32.43A79.93,79.93,0,0,0,174.7,89.71,64,64,0,0,1,224,152Z"
data-flx="ui.icons.locked-announcement-channel-icon.path"
/>
</svg>
);
});
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import type {IconProps} from '@phosphor-icons/react';
import React, {useId} from 'react';
export const LockedForumChannelIcon = React.forwardRef<SVGSVGElement, IconProps>(
({size = 256, className, ...props}, ref) => {
const maskId = useId();
return (
<svg
ref={ref}
width={size}
height={size}
viewBox="0 0 256 256"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
aria-hidden={true}
data-flx="ui.icons.locked-announcement-channel-icon.svg"
{...props}
>
<g mask={`url(#${maskId})`} data-flx="ui.icons.locked-announcement-channel-icon.g">
<path
fill="currentColor"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
d="M169.57,72.59A80,80,0,0,0,16,104v64a16,16,0,0,0,16,16H86.67A80.15,80.15,0,0,0,160,232h64a16,16,0,0,0,16-16V152A80,80,0,0,0,169.57,72.59ZM224,216H160a64.14,64.14,0,0,1-55.68-32.43A79.93,79.93,0,0,0,174.7,89.71,64,64,0,0,1,224,152Z"
data-flx="ui.icons.locked-announcement-channel-icon.path"
/>
</g>
<path
fill="currentColor"
d="M234.25 50.25h-12.5v-9.375c0-4.9728-1.975-9.7419-5.492-13.2583-3.516-3.5163-8.285-5.4917-13.258-5.4917s-9.742 1.9754-13.258 5.4917c-3.517 3.5164-5.492 8.2855-5.492 13.2583v9.375h-12.5c-1.658 0-3.247.6585-4.419 1.8306-1.173 1.1721-1.831 2.7618-1.831 4.4194v43.75c0 1.658.658 3.247 1.831 4.419 1.172 1.173 2.761 1.831 4.419 1.831h62.5c1.658 0 3.247-.658 4.419-1.831 1.173-1.172 1.831-2.761 1.831-4.419V56.5c0-1.6576-.658-3.2473-1.831-4.4194-1.172-1.1721-2.761-1.8306-4.419-1.8306M203 83.0625c-.927 0-1.833-.2749-2.604-.79s-1.372-1.2472-1.727-2.1037c-.354-.8565-.447-1.799-.266-2.7083.18-.9093.627-1.7445 1.282-2.4001.656-.6555 1.491-1.102 2.401-1.2828.909-.1809 1.851-.0881 2.708.2667.856.3548 1.588.9556 2.104 1.7265.515.7708.79 1.6771.79 2.6042 0 1.2432-.494 2.4355-1.373 3.3146-.88.879-2.072 1.3729-3.315 1.3729M215.5 50.25h-25v-9.375c0-3.3152 1.317-6.4946 3.661-8.8388s5.524-3.6612 8.839-3.6612 6.495 1.317 8.839 3.6612 3.661 5.5236 3.661 8.8388z"
data-flx="ui.icons.locked-announcement-channel-icon.path--2"
/>
<defs data-flx="ui.icons.locked-announcement-channel-icon.defs">
<mask id={maskId} data-flx="ui.icons.locked-announcement-channel-icon.mask">
<path fill="white" d="M0 0h256v256H0z" data-flx="ui.icons.locked-announcement-channel-icon.path--3" />
<path fill="black" d="M148 0h108v124H148z" data-flx="ui.icons.locked-announcement-channel-icon.path--4" />
</mask>
</defs>
</svg>
);
},
);
@@ -4,6 +4,7 @@ import {EmailVerificationAlert} from '@app/features/app/components/dialogs/compo
import {UnclaimedAccountAlert} from '@app/features/app/components/dialogs/components/UnclaimedAccountAlert';
import RuntimeConfig from '@app/features/app/state/RuntimeConfig';
import {openClaimAccountModal} from '@app/features/auth/components/modals/ClaimAccountModal';
import {canResendVerificationEmail} from '@app/features/auth/hooks/useVerifyEmailAction';
import {Button} from '@app/features/ui/button/Button';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
@@ -177,7 +178,7 @@ export const AccountTabContent: React.FC<AccountTabProps> = observer(
/>
)}
</AccountSecurityCard>
{RuntimeConfig.emailsEnabled && isClaimed && !usesUsernameSignIn && user.email && !user.verified && (
{canResendVerificationEmail(user) && (
<EmailVerificationAlert data-flx="user.account-security-tab.account-tab.account-tab-content.email-verification-alert" />
)}
</>
+96 -3
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use crate::config::{AppProxyConfig, HttpEndpoint};
use crate::csp::InstanceCspSources;
use axum::http::header;
use reqwest::Url;
use std::sync::Arc;
@@ -15,6 +16,15 @@ const BRANDING_IMAGES: [(&str, &str); 5] = [
("wordmark_url", "FLUXER_APP_WORDMARK_URL"),
("favicon_url", "FLUXER_APP_FAVICON_URL"),
];
const CONNECT_ENDPOINTS: [&str; 7] = [
"api",
"api_client",
"api_public",
"gateway",
"media",
"upload_relay",
"static_cdn",
];
const PRODUCT_NAME: (&str, &str) = ("product_name", "FLUXER_APP_PRODUCT_NAME");
const ICON_URL: (&str, &str) = ("icon_url", "FLUXER_APP_ICON_URL");
const FAVICON_URL: (&str, &str) = ("favicon_url", "FLUXER_APP_FAVICON_URL");
@@ -33,9 +43,45 @@ where
pub fn snapshot_from_discovery(discovery: &serde_json::Value) -> Option<BrandingSnapshot> {
let branding = discovery.pointer("/app_public/branding")?.as_object()?;
Some(snapshot_from_fields(|(key, _)| {
branding.get(key)?.as_str().map(str::to_owned)
}))
Some(BrandingSnapshot {
instance_sources: instance_sources_from_discovery(discovery),
..snapshot_from_fields(|(key, _)| branding.get(key)?.as_str().map(str::to_owned))
})
}
fn instance_sources_from_discovery(discovery: &serde_json::Value) -> InstanceCspSources {
let endpoint = |key: &str| {
discovery
.pointer(&format!("/endpoints/{key}"))
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
};
let http_origin = |key: &'static str| {
endpoint(key).and_then(|value| HttpEndpoint::origin_of(key, value).ok())
};
let mut connect_origins: Vec<String> = Vec::new();
for key in CONNECT_ENDPOINTS {
let Some(origin) = endpoint(key).and_then(connect_origin) else {
continue;
};
if !connect_origins.contains(&origin) {
connect_origins.push(origin);
}
}
InstanceCspSources {
static_cdn_endpoint: http_origin("static_cdn"),
media_endpoint: http_origin("media"),
connect_origins,
}
}
fn connect_origin(value: &str) -> Option<String> {
let url = Url::parse(value).ok()?;
if !matches!(url.scheme(), "http" | "https" | "ws" | "wss") || url.host_str().is_none() {
return None;
}
Some(url.origin().ascii_serialization())
}
fn snapshot_from_fields(
@@ -50,6 +96,7 @@ fn snapshot_from_fields(
let favicon_url = non_empty(field(FAVICON_URL)).or_else(|| icon_url.clone());
BrandingSnapshot {
image_origins: image_origins.into(),
instance_sources: InstanceCspSources::default(),
product_name: non_empty(field(PRODUCT_NAME)).map(Into::into),
icon_url: icon_url.as_deref().and_then(absolute_url).map(Into::into),
favicon_url: favicon_url
@@ -137,6 +184,7 @@ async fn fetch_discovery(
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct BrandingSnapshot {
pub image_origins: Arc<[HttpEndpoint]>,
pub instance_sources: InstanceCspSources,
pub product_name: Option<Arc<str>>,
pub icon_url: Option<Arc<str>>,
pub favicon_url: Option<Arc<str>>,
@@ -163,6 +211,10 @@ impl InstanceBranding {
self.0.borrow().image_origins.clone()
}
pub fn instance_sources(&self) -> InstanceCspSources {
self.0.borrow().instance_sources.clone()
}
pub fn snapshot(&self) -> Arc<BrandingSnapshot> {
self.0.borrow().clone()
}
@@ -191,6 +243,7 @@ impl InstanceBranding {
.collect();
tracing::info!(
?origins,
instance_origins = ?snapshot.instance_sources.connect_origins,
product_name = snapshot.product_name.as_deref(),
icon_url = snapshot.icon_url.as_deref(),
favicon_url = snapshot.favicon_url.as_deref(),
@@ -339,6 +392,46 @@ mod tests {
);
}
#[test]
fn discovery_endpoints_yield_the_instance_origins_the_page_must_reach() {
let discovery = serde_json::json!({
"endpoints": {
"api": "https://chat.example.com/api",
"api_client": "https://chat.example.com/api",
"gateway": "wss://chat.example.com/gateway",
"media": "https://chat.example.com/media",
"static_cdn": "https://static.example.com",
"marketing": "https://www.example.com",
},
"app_public": {"branding": {}},
});
let sources = snapshot_from_discovery(&discovery)
.unwrap()
.instance_sources;
assert_eq!(
sources.connect_origins,
vec![
"https://chat.example.com",
"wss://chat.example.com",
"https://static.example.com",
]
);
assert_eq!(
sources
.media_endpoint
.as_ref()
.map(HttpEndpoint::csp_origin),
Some("https://chat.example.com")
);
assert_eq!(
sources
.static_cdn_endpoint
.as_ref()
.map(HttpEndpoint::csp_origin),
Some("https://static.example.com")
);
}
#[test]
fn a_document_without_branding_is_not_a_discovery_snapshot() {
assert!(snapshot_from_discovery(&serde_json::json!({"api_code_version": "v1"})).is_none());
+158 -22
View File
@@ -80,6 +80,13 @@ fn has_src_attribute(attributes: &str) -> bool {
})
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct InstanceCspSources {
pub static_cdn_endpoint: Option<HttpEndpoint>,
pub media_endpoint: Option<HttpEndpoint>,
pub connect_origins: Vec<String>,
}
#[derive(Clone, Debug, Default)]
pub struct RuntimeCspSources {
pub static_cdn_endpoint: Option<HttpEndpoint>,
@@ -206,6 +213,7 @@ impl CompiledCspPolicy {
&[InlineScriptHash::of("")],
configured_sources,
&[],
&InstanceCspSources::default(),
))
.map_err(CspCompileError::InvalidSpaPolicy)?;
Ok(Self {
@@ -223,12 +231,14 @@ impl CompiledCspPolicy {
&self,
script_hashes: &[InlineScriptHash],
branding_image_origins: &[HttpEndpoint],
instance_sources: &InstanceCspSources,
) -> CspHeaderValues {
let enforced = HeaderValue::from_str(&build_csp(
&self.config,
script_hashes,
&self.runtime_sources,
branding_image_origins,
instance_sources,
))
.expect(
"every CSP source is a validated keyword, scheme, ASCII origin, or base64 hash, so a \
@@ -246,18 +256,27 @@ fn build_csp(
script_hashes: &[InlineScriptHash],
runtime_sources: &RuntimeCspSources,
branding_image_origins: &[HttpEndpoint],
instance_sources: &InstanceCspSources,
) -> String {
build_csp_directives(
config,
Some(script_hashes),
runtime_sources,
branding_image_origins,
instance_sources,
)
.join("; ")
}
fn build_asset_csp(config: &CspConfig, runtime_sources: &RuntimeCspSources) -> String {
build_csp_directives(config, None, runtime_sources, &[]).join("; ")
build_csp_directives(
config,
None,
runtime_sources,
&[],
&InstanceCspSources::default(),
)
.join("; ")
}
fn build_csp_directives(
@@ -265,6 +284,7 @@ fn build_csp_directives(
script_hashes: Option<&[InlineScriptHash]>,
runtime_sources: &RuntimeCspSources,
branding_image_origins: &[HttpEndpoint],
instance_sources: &InstanceCspSources,
) -> Vec<String> {
let mut directives = Vec::with_capacity(14);
@@ -281,17 +301,17 @@ fn build_csp_directives(
script.splice(1..1, hashes.iter().map(|hash| hash.as_source().to_owned()));
}
extend_from(&mut script, &config.extra_script_src, SCRIPT_SOURCES);
extend_runtime_sources(&mut script, runtime_sources, true, false);
extend_runtime_sources(&mut script, runtime_sources, instance_sources, true, false);
directives.push(format!("script-src {}", script.join(" ")));
let mut style = vec!["'self'".to_owned(), "'unsafe-inline'".to_owned()];
extend_from(&mut style, &config.extra_style_src, STYLE_SOURCES);
extend_runtime_sources(&mut style, runtime_sources, true, true);
extend_runtime_sources(&mut style, runtime_sources, instance_sources, true, true);
directives.push(format!("style-src {}", style.join(" ")));
let mut img = vec!["'self'".to_owned(), "blob:".to_owned(), "data:".to_owned()];
extend_from(&mut img, &config.extra_img_src, IMAGE_SOURCES);
extend_runtime_sources(&mut img, runtime_sources, true, true);
extend_runtime_sources(&mut img, runtime_sources, instance_sources, true, true);
for origin in branding_image_origins {
push_endpoint_source(&mut img, Some(origin));
}
@@ -299,18 +319,23 @@ fn build_csp_directives(
let mut media = vec!["'self'".to_owned(), "blob:".to_owned()];
extend_from(&mut media, &config.extra_media_src, MEDIA_SOURCES);
extend_runtime_sources(&mut media, runtime_sources, true, true);
extend_runtime_sources(&mut media, runtime_sources, instance_sources, true, true);
directives.push(format!("media-src {}", media.join(" ")));
let mut font = vec!["'self'".to_owned(), "data:".to_owned()];
extend_from(&mut font, &config.extra_font_src, FONT_SOURCES);
extend_runtime_sources(&mut font, runtime_sources, true, true);
extend_runtime_sources(&mut font, runtime_sources, instance_sources, true, true);
directives.push(format!("font-src {}", font.join(" ")));
let mut connect = vec!["'self'".to_owned(), "blob:".to_owned(), "data:".to_owned()];
extend_from(&mut connect, &config.extra_connect_src, CONNECT_SOURCES);
extend_runtime_sources(&mut connect, runtime_sources, true, true);
extend_runtime_sources(&mut connect, runtime_sources, instance_sources, true, true);
extend_runtime_s3_sources(&mut connect, runtime_sources);
for origin in &instance_sources.connect_origins {
if !connect.contains(origin) {
connect.push(origin.clone());
}
}
directives.push(format!("connect-src {}", connect.join(" ")));
let mut frame = vec!["'self'".to_owned()];
@@ -319,12 +344,18 @@ fn build_csp_directives(
let mut worker = vec!["'self'".to_owned(), "blob:".to_owned()];
extend_from(&mut worker, &config.extra_worker_src, WORKER_SOURCES);
extend_runtime_sources(&mut worker, runtime_sources, true, false);
extend_runtime_sources(&mut worker, runtime_sources, instance_sources, true, false);
directives.push(format!("worker-src {}", worker.join(" ")));
let mut manifest = vec!["'self'".to_owned()];
extend_from(&mut manifest, &config.extra_manifest_src, MANIFEST_SOURCES);
extend_runtime_sources(&mut manifest, runtime_sources, true, false);
extend_runtime_sources(
&mut manifest,
runtime_sources,
instance_sources,
true,
false,
);
directives.push(format!("manifest-src {}", manifest.join(" ")));
directives.push("object-src 'none'".to_owned());
@@ -341,14 +372,17 @@ fn build_csp_directives(
fn extend_runtime_sources(
target: &mut Vec<String>,
runtime_sources: &RuntimeCspSources,
instance_sources: &InstanceCspSources,
include_static: bool,
include_media: bool,
) {
if include_static {
push_endpoint_source(target, runtime_sources.static_cdn_endpoint.as_ref());
push_endpoint_source(target, instance_sources.static_cdn_endpoint.as_ref());
}
if include_media {
push_endpoint_source(target, runtime_sources.media_endpoint.as_ref());
push_endpoint_source(target, instance_sources.media_endpoint.as_ref());
}
}
@@ -448,7 +482,13 @@ mod tests {
#[test]
fn build_csp_includes_required_directives() {
let config = default_csp_config();
let csp = build_csp(&config, &[hash_of("boot()")], &runtime_sources(), &[]);
let csp = build_csp(
&config,
&[hash_of("boot()")],
&runtime_sources(),
&[],
&InstanceCspSources::default(),
);
assert!(csp.contains("default-src"));
assert!(csp.contains("script-src"));
assert!(csp.contains("style-src"));
@@ -467,7 +507,13 @@ mod tests {
#[test]
fn build_csp_allows_the_apex_domain_for_images_and_media() {
let config = default_csp_config();
let csp = build_csp(&config, &[hash_of("boot()")], &runtime_sources(), &[]);
let csp = build_csp(
&config,
&[hash_of("boot()")],
&runtime_sources(),
&[],
&InstanceCspSources::default(),
);
for name in ["img-src ", "media-src "] {
let directive = csp
.split("; ")
@@ -485,7 +531,13 @@ mod tests {
#[test]
fn build_csp_allows_blob_connections_for_camera_background_media() {
let config = default_csp_config();
let csp = build_csp(&config, &[hash_of("boot()")], &runtime_sources(), &[]);
let csp = build_csp(
&config,
&[hash_of("boot()")],
&runtime_sources(),
&[],
&InstanceCspSources::default(),
);
let connect = csp
.split("; ")
.find(|directive| directive.starts_with("connect-src "))
@@ -520,6 +572,7 @@ mod tests {
&[hash_of("first()"), hash_of("second()")],
&runtime_sources(),
&[],
&InstanceCspSources::default(),
);
let script = csp
.split("; ")
@@ -548,6 +601,7 @@ mod tests {
&[hash_of("boot()")],
&runtime_sources(),
&[],
&InstanceCspSources::default(),
);
assert!(!csp.contains("hcaptcha"));
assert!(!csp.contains("challenges.cloudflare.com"));
@@ -556,7 +610,13 @@ mod tests {
#[test]
fn csp_no_double_spaces_or_trailing_semicolons() {
let config = default_csp_config();
let csp = build_csp(&config, &[hash_of("boot()")], &runtime_sources(), &[]);
let csp = build_csp(
&config,
&[hash_of("boot()")],
&runtime_sources(),
&[],
&InstanceCspSources::default(),
);
assert!(!csp.contains(" "), "CSP contains double spaces");
assert!(!csp.ends_with(';'), "CSP ends with semicolon");
assert!(!csp.ends_with("; "), "CSP ends with semicolon+space");
@@ -574,14 +634,26 @@ mod tests {
),
..Default::default()
};
let csp = build_csp(&config, &[hash_of("boot()")], &runtime_sources(), &[]);
let csp = build_csp(
&config,
&[hash_of("boot()")],
&runtime_sources(),
&[],
&InstanceCspSources::default(),
);
assert!(csp.contains("report-uri https://example.com/csp-report"));
}
#[test]
fn build_csp_excludes_report_uri_when_none() {
let config = default_csp_config();
let csp = build_csp(&config, &[hash_of("boot()")], &runtime_sources(), &[]);
let csp = build_csp(
&config,
&[hash_of("boot()")],
&runtime_sources(),
&[],
&InstanceCspSources::default(),
);
assert!(!csp.contains("report-uri"));
}
@@ -593,7 +665,13 @@ mod tests {
media_endpoint: Some(endpoint("https://media.example.test")),
..Default::default()
};
let csp = build_csp(&config, &[hash_of("boot()")], &runtime_sources, &[]);
let csp = build_csp(
&config,
&[hash_of("boot()")],
&runtime_sources,
&[],
&InstanceCspSources::default(),
);
assert!(csp.contains("style-src 'self' 'unsafe-inline'"));
assert!(csp.contains("https://static.example.test"));
assert!(csp.contains("https://media.example.test"));
@@ -635,7 +713,13 @@ mod tests {
..Default::default()
};
let csp = build_csp(&config, &[hash_of("boot()")], &runtime_sources, &[]);
let csp = build_csp(
&config,
&[hash_of("boot()")],
&runtime_sources,
&[],
&InstanceCspSources::default(),
);
assert!(csp.contains("http://localhost:3900"));
assert!(csp.contains("http://fluxer-uploads.localhost:3900"));
@@ -673,7 +757,7 @@ mod tests {
};
let policy = CompiledCspPolicy::compile(default_csp_config(), &configured).unwrap();
let headers = policy.spa_headers(&[hash_of("boot()")], &[]);
let headers = policy.spa_headers(&[hash_of("boot()")], &[], &InstanceCspSources::default());
let header = headers.enforced.to_str().unwrap();
assert!(header.contains(hash_of("boot()").as_source()));
@@ -689,7 +773,7 @@ mod tests {
config.media_endpoint = Some(endpoint("https://media.configured.test/"));
let policy = CompiledCspPolicy::from_config(&config).unwrap();
let headers = policy.spa_headers(&[hash_of("boot()")], &[]);
let headers = policy.spa_headers(&[hash_of("boot()")], &[], &InstanceCspSources::default());
let header = headers.enforced.to_str().unwrap();
for directive in [
"img-src",
@@ -740,8 +824,18 @@ mod tests {
);
let hashes = [hash_of("boot()")];
assert_eq!(
policy.spa_headers(&hashes, &[]).enforced.to_str().unwrap(),
build_csp(&config, &hashes, &sources, &[])
policy
.spa_headers(&hashes, &[], &InstanceCspSources::default())
.enforced
.to_str()
.unwrap(),
build_csp(
&config,
&hashes,
&sources,
&[],
&InstanceCspSources::default()
)
);
}
@@ -753,7 +847,11 @@ mod tests {
endpoint("http://assets.example.test:8443"),
];
let headers = policy.spa_headers(&[hash_of("boot()")], &branding);
let headers = policy.spa_headers(
&[hash_of("boot()")],
&branding,
&InstanceCspSources::default(),
);
let header = headers.enforced.to_str().unwrap();
for directive in header.split("; ") {
@@ -780,6 +878,43 @@ mod tests {
);
}
#[test]
fn discovered_instance_origins_are_granted_to_the_page() {
let policy = CompiledCspPolicy::compile(default_csp_config(), &runtime_sources()).unwrap();
let instance = InstanceCspSources {
static_cdn_endpoint: None,
media_endpoint: Some(endpoint("https://chat.example.com/media")),
connect_origins: vec![
"https://chat.example.com".to_owned(),
"wss://chat.example.com".to_owned(),
],
};
let headers = policy.spa_headers(&[hash_of("boot()")], &[], &instance);
let header = headers.enforced.to_str().unwrap();
let directive = |name: &str| {
header
.split("; ")
.find(|directive| directive.starts_with(&format!("{name} ")))
.unwrap()
.split(' ')
.collect::<Vec<_>>()
};
assert!(directive("connect-src").contains(&"wss://chat.example.com"));
assert!(directive("connect-src").contains(&"https://chat.example.com"));
assert!(directive("img-src").contains(&"https://chat.example.com"));
assert!(directive("media-src").contains(&"https://chat.example.com"));
assert!(!directive("script-src").contains(&"https://chat.example.com"));
assert!(
!policy
.asset_header()
.to_str()
.unwrap()
.contains("chat.example.com")
);
}
#[test]
fn a_branding_image_origin_the_policy_already_grants_is_not_repeated() {
let configured = RuntimeCspSources {
@@ -791,6 +926,7 @@ mod tests {
let headers = policy.spa_headers(
&[hash_of("boot()")],
&[endpoint("https://media.example.test")],
&InstanceCspSources::default(),
);
let header = headers.enforced.to_str().unwrap();
let img = header
+2
View File
@@ -226,6 +226,7 @@ async fn serve_spa_index(state: &AppState, headers: &HeaderMap, request_path: &s
let csp = state.csp.spa_headers(
&inline_script_hashes(&html),
&state.branding.image_origins(),
&state.branding.instance_sources(),
);
build_spa_response(html.into_boxed_str(), &csp, should_bust_dev_assets)
}
@@ -1050,6 +1051,7 @@ mod tests {
icon_url: Some(Arc::from("https://example.test/icon.png")),
favicon_url: Some(Arc::from("https://example.test/favicon.png")),
theme_color: Some(Arc::from("#7b2ce4")),
..BrandingSnapshot::default()
}
}
+50 -5
View File
@@ -63,7 +63,22 @@ if (!channel) {
const isCanary = buildChannel === 'canary';
const isStable = buildChannel === 'stable';
const isLinuxBuild = process.argv.includes('--linux');
const platformArguments = new Set(process.argv.map((argument) => argument.split('=', 1)[0]));
const linuxRequested = ['--linux', '-l'].some((selector) => platformArguments.has(selector));
const macRequested = ['--mac', '--macos', '-m', '-o'].some((selector) => platformArguments.has(selector));
const windowsRequested = ['--win', '--windows', '-w'].some((selector) => platformArguments.has(selector));
const targetPlatform = linuxRequested
? 'linux'
: macRequested
? 'darwin'
: windowsRequested
? 'win32'
: process.platform;
const isLinuxBuild = targetPlatform === 'linux';
const customLinuxElectron =
isLinuxBuild && process.env.FLUXER_ELECTRON_DIST?.trim() ? require('./scripts/custom-electron.cjs') : null;
const linuxElectronDist = customLinuxElectron?.resolveLinuxElectronDist() ?? null;
const verifiedLinuxElectronArtifacts = new Map();
const productName = channel.productName;
const linuxOptDirName = channel.linuxOptDirName;
const installedProductName = isLinuxBuild ? linuxOptDirName : productName;
@@ -99,9 +114,6 @@ const legacyLinuxCanaryOptDir = '/opt/Fluxer Canary';
const legacyLinuxOptDirSweepScript = path.resolve(__dirname, 'packaging/linux/rpm-post-transaction.sh');
const legacyLinuxOptDirRpmFpmArgs = isCanary ? ['--rpm-posttrans', legacyLinuxOptDirSweepScript] : [];
const macOSMinimumSystemVersion = '13.0';
const isMacBuild = process.argv.includes('--mac');
const isWindowsBuild = process.argv.includes('--win');
const targetPlatform = isLinuxBuild ? 'linux' : isMacBuild ? 'darwin' : isWindowsBuild ? 'win32' : process.platform;
const metadataName = isLinuxBuild ? linuxPackageName : packageName;
const shellVersion = process.env.VERSION || readBundledRendererVersion();
const supportedTargetArchs = ['x64', 'arm64'];
@@ -797,6 +809,27 @@ async function verifyNativePackageInputs(context) {
await verifyDarwinNativeArchitectures(platform, arch, entries, 'before packaging');
}
async function beforePack(context) {
if (linuxElectronDist && context.electronPlatformName !== 'linux') {
throw new Error(
'The patched Linux Electron distribution cannot package other platforms. Use separate electron-builder invocations for Linux and macOS or Windows.',
);
}
if (context.electronPlatformName === 'linux') {
if (!linuxElectronDist) {
throw new Error(
'Linux packaging requires patched Electron. Set FLUXER_ELECTRON_DIST to the directory containing its ZIP and build manifest.',
);
}
const arch = context.arch === 1 ? 'x64' : context.arch === 3 ? 'arm64' : context.arch;
verifiedLinuxElectronArtifacts.set(
arch,
await customLinuxElectron.verifyLinuxElectronArchive(linuxElectronDist, arch),
);
}
await verifyNativePackageInputs(context);
}
async function verifyPackagedNativeArtifacts(context) {
const platform = context.electronPlatformName;
const arch = normalizeArch(context.arch);
@@ -1147,6 +1180,15 @@ async function readAsarEntry(asarPath, entryPath) {
}
async function afterPack(context) {
if (context.electronPlatformName === 'linux') {
if (!customLinuxElectron) {
throw new Error('Custom Electron was not configured before Linux packaging.');
}
await customLinuxElectron.verifyPackagedLinuxElectron(
path.join(context.appOutDir, linuxPackageName),
verifiedLinuxElectronArtifacts.get(normalizeArch(context.arch)),
);
}
await copyMissingPackagedNativeArtifacts(context);
await cleanupNativeBuildIntermediates(context);
await addLinuxLegacyBinarySymlink(context);
@@ -1613,6 +1655,9 @@ async function verifyLinuxArtifactContracts(buildResult) {
}
module.exports = {
...(customLinuxElectron
? {electronVersion: customLinuxElectron.expectedElectronVersion, electronDist: linuxElectronDist}
: {}),
appId,
productName: installedProductName,
copyright: 'Copyright © 2026 Fluxer Platform AB',
@@ -1732,7 +1777,7 @@ module.exports = {
schemes: [channel.protocolScheme],
},
],
beforePack: verifyNativePackageInputs,
beforePack,
afterPack,
afterAllArtifactBuild: verifyLinuxArtifactContracts,
toolsets: {
+335
View File
@@ -0,0 +1,335 @@
#!/usr/bin/env bash
set -euo pipefail
readonly ELECTRON_VERSION=44.4.1
readonly ELECTRON_REVISION=09d1a6590e3148a1a2132b07a5b107b7c2414191
readonly DEPOT_TOOLS_REVISION=889099791deff38466d471fa6590951c91a1d8ab
readonly BUILD_IMAGE='ghcr.io/electron/build:daad061f4b99a0ae1c841be4aa09188280a9c8a4@sha256:7a161ad561103ebbfb5ff18eaf47cb579ca0c528bb7ec9ed8cad30a41d5fa63e'
readonly PATCH_NAME=fix_synchronize_vaapi_surfaces_before_output.patch
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
readonly SCRIPT_DIR
die() { printf 'Error: %s\n' "$*" >&2; exit 1; }
usage() {
cat <<'USAGE'
Build patched Electron 44.4.1 on a Linux x64 host with Podman or Docker.
Usage: build-linux.sh [--arch x64|arm64|all] [--build-root PATH] [--output-dir PATH] [--check]
--arch Target architecture; all builds x64 then arm64 (default: all).
--build-root Dedicated persistent cache, source and output tree.
Default: ${XDG_CACHE_HOME:-$HOME/.cache}/fluxer/electron-44.4.1
--output-dir Distribution ZIPs and .fluxer.json manifests (default: BUILD_ROOT/artifacts).
--check Validate host resources and configuration without pulling or compiling.
Requires at least 300 GiB free in the build filesystem. Recommended/default
container memory is 48 GiB; use CONTAINER_MEMORY=32g for the lower-memory minimum.
Available host RAM must cover the selected container limit.
The build uses host networking and builds architectures sequentially.
Optional environment: CONTAINER_ENGINE=podman|docker, CONTAINER_CPUS=8,
CONTAINER_MEMORY=48g, CONTAINER_CPUSET=0-7, BUILD_JOBS=8.
Without CONTAINER_CPUSET, CPU affinity is limited to the smaller CPU/job budget
using CPUs permitted by the host. An explicit CPU set overrides that selection.
The container runs as root; completed archives are returned to the invoking UID/GID.
USAGE
}
inside_container() {
[[ ${FLUXER_ELECTRON_CONTAINER:-} == 1 && "$PWD" == /work ]] || die 'Internal container invocation is invalid.'
export DEPOT_TOOLS_UPDATE=0
export DEPOT_TOOLS_METRICS=0
export GIT_TERMINAL_PROMPT=0
export GIT_CACHE_PATH=/work/git-cache
export PATH="/work/depot_tools:$PATH"
local arch
checkout_pinned depot_tools https://chromium.googlesource.com/chromium/tools/depot_tools.git "$DEPOT_TOOLS_REVISION"
[[ -z $(git -C depot_tools status --porcelain --untracked-files=no) ]] || die 'Cached depot_tools contains tracked edits.'
DEPOT_TOOLS_DIR=/work/depot_tools /work/depot_tools/ensure_bootstrap
[[ -s depot_tools/python3_bin_reldir.txt ]] || die 'Pinned depot_tools Python bootstrap did not complete.'
depot_tools/python-bin/python3 --version
checkout_pinned src/electron https://github.com/electron/electron.git "$ELECTRON_REVISION"
python3 - "$SCRIPT_DIR/patches/$PATCH_NAME" "src/electron/patches/chromium/$PATCH_NAME" <<'PY'
import pathlib
import shutil
import subprocess
import sys
source, destination = map(pathlib.Path, sys.argv[1:])
allowed = {"patches/chromium/.patches", "patches/chromium/" + destination.name}
changes = subprocess.check_output(
["git", "-C", "src/electron", "status", "--porcelain=v1", "-z"], text=True
).split("\0")
for change in filter(None, changes):
if change[:2].strip() not in {"M", "??"} or change[3:] not in allowed:
raise SystemExit("Cached Electron has unrelated edits: " + repr(change))
manifest = destination.parent / ".patches"
upstream = subprocess.check_output(
["git", "-C", "src/electron", "show", "HEAD:patches/chromium/.patches"]
)
patched = upstream.rstrip(b"\n") + b"\n" + destination.name.encode() + b"\n"
if manifest.read_bytes() not in (upstream, patched):
raise SystemExit("Cached Chromium patch manifest differs from the pinned upstream series")
shutil.copyfile(source, destination)
manifest.write_bytes(patched)
PY
gclient config --name src/electron --unmanaged --custom-var=checkout_arm64=True https://github.com/electron/electron
gclient sync --no-history --with_branch_heads --with_tags --jobs "$BUILD_JOBS"
git -C src apply --reverse --check "$SCRIPT_DIR/patches/$PATCH_NAME"
for arch in "$@"; do
python3 src/electron/script/pgo/download-profiles.py --targets "linux-$arch,v8-builtins"
(
cd src
gn gen "out/Release-$arch" --fail-on-unused-args --args="import(\"//electron/build/args/release.gn\") target_cpu=\"$arch\" override_electron_version=\"$ELECTRON_VERSION\" use_remoteexec=false"
python3 - "$arch" <<'PY'
import json
import subprocess
import sys
arch = sys.argv[1]
arguments = json.loads(subprocess.check_output(
["gn", "args", "out/Release-" + arch, "--list", "--short", "--json"], text=True
))
resolved = {
argument["name"]: argument.get("current", argument["default"])["value"]
for argument in arguments
}
expected = {
"target_cpu": json.dumps(arch),
"use_vaapi": "true",
"enable_platform_hevc": "true",
"enable_hevc_parser_and_hw_decoder": "true",
}
for name, value in expected.items():
if resolved.get(name) != value:
raise SystemExit(f"Electron build requires {name} = {value}; resolved {resolved.get(name)!r}")
print(f"Verified GN argument: {name} = {value}", flush=True)
PY
autoninja -C "out/Release-$arch" -j "$BUILD_JOBS" electron:electron_dist_zip
python3 electron/script/zip_manifests/check-zip-manifest.py "out/Release-$arch/dist.zip" "electron/script/zip_manifests/dist_zip.linux.$arch.manifest"
)
python3 - "$arch" "$ELECTRON_VERSION" "$ELECTRON_REVISION" "$SCRIPT_DIR/patches/$PATCH_NAME" <<'PY'
import hashlib
import json
import os
import pathlib
import shutil
import struct
import sys
import tempfile
import zipfile
arch, version, revision, patch_name = sys.argv[1:]
source = pathlib.Path("src/out/Release-" + arch + "/dist.zip")
with zipfile.ZipFile(source) as archive:
if archive.testzip() is not None:
raise SystemExit("Built Electron ZIP has a corrupt member")
with archive.open("electron") as executable:
header = executable.read(64)
if len(header) != 64 or header[:6] != b"\x7fELF\x02\x01" or struct.unpack_from("<H", header, 18)[0] != {"x64": 62, "arm64": 183}[arch]:
raise SystemExit("Built Electron executable has the wrong ELF architecture")
marker = b"failed to synchronize VA surface before output"
tail = header
has_patch_marker = marker in header
while chunk := executable.read(1024 * 1024):
window = tail + chunk
has_patch_marker = has_patch_marker or marker in window
tail = window[-(len(marker) - 1):]
if not has_patch_marker:
raise SystemExit("Built Electron executable is missing the VA-API synchronization patch marker")
if archive.read("version").decode().strip() != version:
raise SystemExit("Built Electron version differs from the pinned version")
def sha256(path):
digest = hashlib.sha256()
with open(path, "rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
output = pathlib.Path("/artifacts")
name = "electron-v" + version + "-linux-" + arch
archive_path = output / (name + ".zip")
manifest_path = output / (name + ".fluxer.json")
manifest = {
"schemaVersion": 1,
"electronVersion": version,
"electronRevision": revision,
"chromiumPatchSha256": sha256(patch_name),
"platform": "linux",
"arch": arch,
"archiveName": archive_path.name,
"archiveSha256": sha256(source),
}
for destination, contents in [(archive_path, None), (manifest_path, json.dumps(manifest, indent=2) + "\n")]:
descriptor, temporary = tempfile.mkstemp(prefix="." + destination.name + ".", dir=output)
try:
with os.fdopen(descriptor, "wb") as stream:
if contents is None:
with source.open("rb") as runtime:
shutil.copyfileobj(runtime, stream)
else:
stream.write(contents.encode())
os.chmod(temporary, 0o644)
os.chown(temporary, int(os.environ["FLUXER_HOST_UID"]), int(os.environ["FLUXER_HOST_GID"]))
os.replace(temporary, destination)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
print("Prepared " + str(archive_path) + " and " + str(manifest_path), flush=True)
PY
done
}
checkout_pinned() {
local directory=$1 remote=$2 revision=$3 current
if [[ ! -d $directory/.git ]]; then
[[ ! -e $directory || -z $(ls -A "$directory") ]] || die "Source directory is not an empty Git checkout: $directory"
git init -q "$directory"
fi
if ! git -C "$directory" remote get-url origin >/dev/null 2>&1; then
git -C "$directory" remote add origin "$remote"
fi
[[ $(git -C "$directory" remote get-url origin) == "$remote" ]] || die "Cached Git remote differs: $directory"
if ! current=$(git -C "$directory" rev-parse --verify HEAD 2>/dev/null); then
git -C "$directory" fetch --depth=1 origin "$revision"
git -C "$directory" checkout --detach FETCH_HEAD
current=$(git -C "$directory" rev-parse HEAD)
fi
[[ $current == "$revision" ]] || die "Cached revision differs in $directory; choose a new build root."
}
if [[ ${1:-} == --inside-container ]]; then
shift
inside_container "$@"
exit
fi
ARCH=all
BUILD_ROOT="${XDG_CACHE_HOME:-$HOME/.cache}/fluxer/electron-$ELECTRON_VERSION"
OUTPUT_DIR=''
CHECK_ONLY=0
while (($#)); do
case $1 in
--arch|--build-root|--output-dir)
(($# >= 2)) || die "Missing value for $1"
case $1 in
--arch) ARCH=$2 ;;
--build-root) BUILD_ROOT=$2 ;;
--output-dir) OUTPUT_DIR=$2 ;;
esac
shift 2 ;;
--check) CHECK_ONLY=1; shift ;;
--help|-h) usage; exit ;;
*) die "Unknown argument: $1" ;;
esac
done
case $ARCH in
all) ARCHES=(x64 arm64) ;;
x64|arm64) ARCHES=("$ARCH") ;;
*) die '--arch must be x64, arm64 or all.' ;;
esac
[[ $(uname -s) == Linux && $(uname -m) == x86_64 ]] || die 'Build on a Linux x64 host; macOS and emulated Linux builds are unsupported.'
for command in python3 flock; do command -v "$command" >/dev/null || die "Required command is unavailable: $command"; done
if [[ -z ${CONTAINER_ENGINE:-} ]]; then
if command -v podman >/dev/null; then CONTAINER_ENGINE=podman; else CONTAINER_ENGINE=docker; fi
fi
case $CONTAINER_ENGINE in podman|docker) ;; *) die 'CONTAINER_ENGINE must be podman or docker.' ;; esac
command -v "$CONTAINER_ENGINE" >/dev/null || die "Required container engine is unavailable: $CONTAINER_ENGINE"
BUILD_JOBS=${BUILD_JOBS:-8}
CONTAINER_CPUS=${CONTAINER_CPUS:-8}
CONTAINER_MEMORY=${CONTAINER_MEMORY:-48g}
[[ $BUILD_JOBS =~ ^[1-9][0-9]*$ ]] || die 'BUILD_JOBS must be a positive integer.'
[[ $CONTAINER_CPUS =~ ^[1-9][0-9]*$ ]] || die 'CONTAINER_CPUS must be a positive integer.'
CONTAINER_CPUSET=$(python3 - "$CONTAINER_CPUS" "$BUILD_JOBS" "${CONTAINER_CPUSET:-}" <<'PY'
import os
import re
import sys
allowed = sorted(os.sched_getaffinity(0))
if not allowed:
raise SystemExit("No CPUs are permitted by the host affinity")
explicit = sys.argv[3]
if explicit:
if not re.fullmatch(r"[0-9]+(?:-[0-9]+)?(?:,[0-9]+(?:-[0-9]+)?)*", explicit):
raise SystemExit("CONTAINER_CPUSET must be a comma-separated CPU list or ranges, e.g. 0-7,9")
requested = set()
for part in explicit.split(","):
bounds = [int(value) for value in part.split("-")]
first, last = bounds[0], bounds[-1]
if first > last or last > allowed[-1]:
raise SystemExit("CONTAINER_CPUSET contains an invalid or unavailable CPU range: " + part)
requested.update(range(first, last + 1))
unavailable = requested.difference(allowed)
if unavailable:
raise SystemExit("CONTAINER_CPUSET includes CPUs outside the host affinity: " + ",".join(map(str, sorted(unavailable))))
print(explicit)
else:
budget = min(int(sys.argv[1]), int(sys.argv[2]), len(allowed))
print(",".join(map(str, allowed[:budget])))
PY
)
BUILD_ROOT=$(python3 -c 'import pathlib,sys; print(pathlib.Path(sys.argv[1]).expanduser().resolve())' "$BUILD_ROOT")
OUTPUT_DIR=${OUTPUT_DIR:-$BUILD_ROOT/artifacts}
OUTPUT_DIR=$(python3 -c 'import pathlib,sys; print(pathlib.Path(sys.argv[1]).expanduser().resolve())' "$OUTPUT_DIR")
python3 - "$BUILD_ROOT" "$CONTAINER_MEMORY" "$CONTAINER_CPUS" <<'PY'
import os
import pathlib
import re
import shutil
import sys
root = pathlib.Path(sys.argv[1])
if root == pathlib.Path("/") or root == pathlib.Path.home():
raise SystemExit("Choose a dedicated build root, not / or your home directory")
probe = root
while not probe.exists():
probe = probe.parent
free = shutil.disk_usage(probe).free / 2**30
if free < 300:
raise SystemExit(f"Build filesystem has {free:.1f} GiB free; at least 300 GiB is required")
memory = dict(line.split(":", 1) for line in pathlib.Path("/proc/meminfo").read_text().splitlines())
total = int(memory["MemTotal"].split()[0]) * 1024
available = int(memory["MemAvailable"].split()[0]) * 1024
match = re.fullmatch(r"([1-9][0-9]*)([mgMG])", sys.argv[2])
if not match:
raise SystemExit("CONTAINER_MEMORY must use whole megabytes or gigabytes, e.g. 48g")
limit = int(match[1]) * {"m": 2**20, "g": 2**30}[match[2].lower()]
if total < 31 * 2**30 or limit < 32 * 2**30 or available < limit:
raise SystemExit(f"Insufficient RAM: host {total / 2**30:.1f} GiB total, {available / 2**30:.1f} GiB available, container limit {limit / 2**30:.1f} GiB; need at least a 32 GiB container limit and available RAM covering that limit")
if int(sys.argv[3]) > (os.cpu_count() or 1):
raise SystemExit("CONTAINER_CPUS exceeds the host's logical CPU count")
print(f"Host ready: {free:.1f} GiB free, {available / 2**30:.1f} GiB available RAM; container limit {limit / 2**30:.1f} GiB", flush=True)
PY
"$CONTAINER_ENGINE" info >/dev/null
printf 'Engine: %s\nImage: %s\nBuild root: %s\nOutput: %s\nTargets: %s\nCPU set: %s\n' "$CONTAINER_ENGINE" "$BUILD_IMAGE" "$BUILD_ROOT" "$OUTPUT_DIR" "${ARCHES[*]}" "$CONTAINER_CPUSET"
((CHECK_ONLY == 0)) || exit
if [[ -d $BUILD_ROOT && ! -f $BUILD_ROOT/.fluxer-electron-build-id && -n $(ls -A "$BUILD_ROOT") ]]; then
die 'Build root is not empty and has no ownership marker; choose a dedicated empty directory.'
fi
mkdir -p "$BUILD_ROOT" "$OUTPUT_DIR"
exec 9>"$BUILD_ROOT/.fluxer-electron-build.lock"
flock -n 9 || die 'Another build is using this build root.'
if [[ -f $BUILD_ROOT/.fluxer-electron-build-id ]]; then
[[ $(cat "$BUILD_ROOT/.fluxer-electron-build-id") == "$ELECTRON_REVISION" ]] || die 'Build root belongs to a different Electron revision.'
else
printf '%s\n' "$ELECTRON_REVISION" > "$BUILD_ROOT/.fluxer-electron-build-id"
fi
mkdir -p "$BUILD_ROOT/container-home" "$BUILD_ROOT/git-cache"
ENGINE_ARGS=(run --rm --platform linux/amd64 --user 0 --network host --workdir /work --cpus "$CONTAINER_CPUS" --cpuset-cpus "$CONTAINER_CPUSET" --memory "$CONTAINER_MEMORY")
ENGINE_ARGS+=(
--volume "$BUILD_ROOT:/work"
--volume "$OUTPUT_DIR:/artifacts"
--volume "$BUILD_ROOT/container-home:/root"
--volume "$SCRIPT_DIR:/fluxer-electron:ro"
--env FLUXER_ELECTRON_CONTAINER=1
--env "FLUXER_HOST_UID=$(id -u)"
--env "FLUXER_HOST_GID=$(id -g)"
--env "BUILD_JOBS=$BUILD_JOBS"
)
"$CONTAINER_ENGINE" pull "$BUILD_IMAGE"
"$CONTAINER_ENGINE" "${ENGINE_ARGS[@]}" "$BUILD_IMAGE" bash /fluxer-electron/build-linux.sh --inside-container "${ARCHES[@]}"
@@ -0,0 +1,78 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Fluxer <[email protected]>
Date: Sat, 17 Oct 2026 14:10:46 +0000
Subject: fix: synchronize VA-API surfaces before output
diff --git a/media/gpu/vaapi/vaapi_video_decoder.cc b/media/gpu/vaapi/vaapi_video_decoder.cc
--- a/media/gpu/vaapi/vaapi_video_decoder.cc
+++ b/media/gpu/vaapi/vaapi_video_decoder.cc
@@ -223,6 +223,7 @@
transcryption_ = false;
SetState(State::kUninitialized);
+ surface_sync_failed_ = false;
}
DCHECK(!current_decode_task_);
DCHECK(decode_task_queue_.empty());
@@ -404,6 +405,11 @@
{
TRACE_EVENT("media,gpu", "VaapiVideoDecoder::Decode");
decode_result = decoder_->Decode();
+ }
+ if (surface_sync_failed_) {
+ SetState({DecoderStatus::Codes::kFailed,
+ "failed to synchronize VA surface before output"});
+ return;
}
switch (decode_result) {
case AcceleratedVideoDecoder::kRanOutOfStreamData:
@@ -559,6 +565,10 @@
DVLOGF(4);
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK_EQ(state_, State::kDecoding);
+ if (surface_sync_failed_) {
+ return;
+ }
+
CHECK_NE(va_surface_id, VA_INVALID_SURFACE);
// Find the timestamp associated with |buffer_id|. It's possible that a
@@ -624,6 +634,11 @@
gfx::HDRMetadata hdr_metadata = static_hdr_metadata_;
hdr_metadata.MergeMetadataFrom(dynamic_hdr_metadata);
frame->set_hdr_metadata(hdr_metadata);
+
+ if (!vaapi_wrapper_->SyncSurface(va_surface_id)) {
+ surface_sync_failed_ = true;
+ return;
+ }
output_cb_.Run(std::move(frame));
}
@@ -1045,7 +1060,13 @@
// Flush will block until SurfaceReady() has been called for every frame
// currently decoding.
- if (!decoder_->Flush()) {
+ const bool flush_succeeded = decoder_->Flush();
+ if (surface_sync_failed_) {
+ SetState({DecoderStatus::Codes::kFailed,
+ "failed to synchronize VA surface before output"});
+ return;
+ }
+ if (!flush_succeeded) {
SetState({DecoderStatus::Codes::kFailed, "Failed to flush the decoder"});
return;
}
diff --git a/media/gpu/vaapi/vaapi_video_decoder.h b/media/gpu/vaapi/vaapi_video_decoder.h
--- a/media/gpu/vaapi/vaapi_video_decoder.h
+++ b/media/gpu/vaapi/vaapi_video_decoder.h
@@ -211,6 +211,8 @@
// The video decoder's state.
State state_ GUARDED_BY_CONTEXT(sequence_checker_) = State::kUninitialized;
+ bool surface_sync_failed_ GUARDED_BY_CONTEXT(sequence_checker_) = false;
+
// Callback used to notify the client when a frame is available for output.
PipelineOutputCB output_cb_ GUARDED_BY_CONTEXT(sequence_checker_);
+138
View File
@@ -0,0 +1,138 @@
const {execFile} = require('node:child_process');
const {createHash} = require('node:crypto');
const {createReadStream, statSync} = require('node:fs');
const fs = require('node:fs/promises');
const path = require('node:path');
const {promisify} = require('node:util');
const execFileAsync = promisify(execFile);
const expectedElectronVersion = '44.4.1';
const expectedElectronRevision = '09d1a6590e3148a1a2132b07a5b107b7c2414191';
const chromiumPatchPath = path.join(
__dirname,
'../electron/patches/fix_synchronize_vaapi_surfaces_before_output.patch',
);
function resolveLinuxElectronDist() {
const configuredDirectory = process.env.FLUXER_ELECTRON_DIST?.trim();
if (!configuredDirectory) {
throw new Error(
'Linux packaging requires patched Electron. Set FLUXER_ELECTRON_DIST to the directory containing the custom Electron ZIP and its .fluxer.json build manifest.',
);
}
const directory = path.resolve(configuredDirectory);
if (!statSync(directory, {throwIfNoEntry: false})?.isDirectory()) {
throw new Error(`FLUXER_ELECTRON_DIST is not a directory: ${directory}`);
}
return directory;
}
async function sha256File(filePath) {
const hash = createHash('sha256');
for await (const chunk of createReadStream(filePath)) hash.update(chunk);
return hash.digest('hex');
}
const inspectArchive = `
import hashlib, json, stat, struct, sys, zipfile
archive_path, arch, version = sys.argv[1:]
with zipfile.ZipFile(archive_path) as archive:
for required in ('electron', 'version', 'LICENSE', 'LICENSES.chromium.html'):
matches = [entry for entry in archive.infolist() if entry.filename == required]
if len(matches) != 1 or matches[0].is_dir() or stat.S_ISLNK(matches[0].external_attr >> 16):
raise ValueError('Expected one regular ZIP entry: ' + required)
if archive.read('version').decode('utf-8').strip() != version:
raise ValueError('Electron ZIP version does not match ' + version)
with archive.open('electron') as executable:
header = executable.read(20)
if len(header) != 20 or header[:6] != b'\\x7fELF\\x02\\x01':
raise ValueError('Electron must be a 64-bit little-endian ELF executable')
if struct.unpack_from('<H', header, 18)[0] != {'x64': 62, 'arm64': 183}[arch]:
raise ValueError('Electron ELF architecture does not match ' + arch)
digest = hashlib.sha256(header)
marker = b'failed to synchronize VA surface before output'
tail = header
has_patch_marker = marker in header
while chunk := executable.read(1024 * 1024):
digest.update(chunk)
window = tail + chunk
has_patch_marker = has_patch_marker or marker in window
tail = window[-(len(marker) - 1):]
if not has_patch_marker:
raise ValueError('Electron executable is missing the VA-API synchronization patch marker')
print(json.dumps({'binarySha256': digest.hexdigest()}))
`;
async function verifyLinuxElectronArchive(directory, arch) {
if (!['x64', 'arm64'].includes(arch)) {
throw new Error(`Custom Linux Electron does not support architecture: ${arch}`);
}
const artifactName = `electron-v${expectedElectronVersion}-linux-${arch}`;
const archiveName = `${artifactName}.zip`;
const manifestPath = path.join(directory, `${artifactName}.fluxer.json`);
let manifest;
try {
manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
} catch (error) {
throw new Error(`Cannot read custom Electron build manifest ${manifestPath}: ${error.message}`);
}
const expectedFields = {
schemaVersion: 1,
electronVersion: expectedElectronVersion,
electronRevision: expectedElectronRevision,
platform: 'linux',
arch,
archiveName,
};
for (const [field, expected] of Object.entries(expectedFields)) {
if (manifest?.[field] !== expected) {
throw new Error(`Custom Electron manifest ${field} must be ${JSON.stringify(expected)}: ${manifestPath}`);
}
}
const patchSha256 = await sha256File(chromiumPatchPath);
const acceptedPatchSha256s = new Set([patchSha256]);
if (patchSha256 === '21ea23b8a566f3f2dbdb69d0b361039dd9128c5e34a7ae89a8a61d7099808b8e') {
acceptedPatchSha256s.add('d39d05d4a00cd09eca43c7cd83383eaefa18cae7ec1b7efc26987bdc0f23c83e');
}
if (!acceptedPatchSha256s.has(manifest?.chromiumPatchSha256)) {
throw new Error(`Custom Electron manifest chromiumPatchSha256 does not match the current patch: ${manifestPath}`);
}
if (!/^[a-f0-9]{64}$/.test(manifest.archiveSha256)) {
throw new Error(`Custom Electron manifest has no valid archiveSha256: ${manifestPath}`);
}
const archivePath = path.join(directory, archiveName);
if ((await sha256File(archivePath)) !== manifest.archiveSha256) {
throw new Error(`Custom Electron ZIP checksum does not match its build manifest: ${archivePath}`);
}
let inspection;
try {
const {stdout} = await execFileAsync(
'python3',
['-c', inspectArchive, archivePath, arch, expectedElectronVersion],
{
maxBuffer: 64 * 1024,
},
);
inspection = JSON.parse(stdout);
} catch (error) {
throw new Error(`Cannot verify custom Electron ZIP ${archivePath}: ${error.stderr?.trim() || error.message}`);
}
if (!/^[a-f0-9]{64}$/.test(inspection.binarySha256)) {
throw new Error(`Custom Electron ZIP inspection returned no executable checksum: ${archivePath}`);
}
return {manifest, binarySha256: inspection.binarySha256};
}
async function verifyPackagedLinuxElectron(executablePath, artifact) {
if (!artifact) throw new Error('Custom Electron archive was not verified before Linux packaging.');
if ((await sha256File(executablePath)) !== artifact.binarySha256) {
throw new Error(`Packaged Linux executable does not match the verified custom Electron build: ${executablePath}`);
}
}
module.exports = {
expectedElectronVersion,
resolveLinuxElectronDist,
verifyLinuxElectronArchive,
verifyPackagedLinuxElectron,
};
@@ -294,6 +294,11 @@ export function appendLinuxChromiumFlagsConfig(channel: BuildChannel): void {
}
}
export function addLinuxHardwareVideoDecodeFeatures(features: Set<string>): void {
if (process.platform !== 'linux') return;
features.add('VaapiOnNvidiaGPUs');
}
export function addLinuxHardwareVideoEncodeFeatures(features: Set<string>): void {
if (process.platform !== 'linux') return;
features.add('AcceleratedVideoEncoder');
+30 -3
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import child_process from 'node:child_process';
import crypto from 'node:crypto';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
@@ -27,6 +28,7 @@ const LEGACY_DESKTOP_FILE_BASENAME = `${LEGACY_LINUX_DESKTOP_ENTRY_ID}.desktop`;
const GENERIC_NAME_DEFAULT = 'Instant Messenger';
const COMMENT_DEFAULT = 'Instant messaging and VoIP';
export const GENERATED_MARKER = '# X-Generated-By=fluxer-desktop';
const GENERATED_DIGEST_PREFIX = '# X-Generated-Digest=';
const DESKTOP_ACTIONS = [
{
id: 'open-settings',
@@ -137,7 +139,7 @@ function buildLocalizedEntryLines(key: string, defaultValue: string, localizedVa
return lines;
}
export function buildDesktopFileContents(execPath: string, hidden: boolean): string {
function buildDesktopFileBody(execPath: string, hidden: boolean): string {
const execLine = `${quoteExecArg(execPath)} %U`;
return [
'[Desktop Entry]',
@@ -162,6 +164,18 @@ export function buildDesktopFileContents(execPath: string, hidden: boolean): str
].join('\n');
}
function digestDesktopFileBody(body: string): string {
return crypto.createHash('sha256').update(body, 'utf8').digest('hex');
}
export function buildDesktopFileContents(execPath: string, hidden: boolean): string {
const body = buildDesktopFileBody(execPath, hidden);
return body.replace(
`${GENERATED_MARKER}\n`,
`${GENERATED_MARKER}\n${GENERATED_DIGEST_PREFIX}${digestDesktopFileBody(body)}\n`,
);
}
function readDesktopEntryValue(contents: string, key: string): string | null {
for (const line of contents.split('\n')) {
const trimmed = line.trim();
@@ -187,6 +201,18 @@ function isStaleDesktopFile(contents: string): boolean {
return !isExecutableFile(tryExec);
}
function isUnmodifiedGeneratedFile(contents: string, execPath: string): boolean {
if (!contents.includes(GENERATED_MARKER)) return false;
const lines = contents.split('\n');
const digestIndex = lines.findIndex((line) => line.startsWith(GENERATED_DIGEST_PREFIX));
if (digestIndex === -1) {
return contents === buildDesktopFileBody(execPath, false) || contents === buildDesktopFileBody(execPath, true);
}
const digest = lines[digestIndex].slice(GENERATED_DIGEST_PREFIX.length).trim();
const body = lines.filter((_, index) => index !== digestIndex).join('\n');
return digestDesktopFileBody(body) === digest;
}
function readExistingDesktopFile(filePath: string): string | null {
try {
return fs.readFileSync(filePath, 'utf8');
@@ -280,14 +306,15 @@ function syncUserDesktopEntry(): boolean {
const filePath = getDesktopFilePath();
const existing = readExistingDesktopFile(filePath);
const systemEntry = findSystemDesktopEntry();
const owned = existing !== null && (isUnmodifiedGeneratedFile(existing, execPath) || isStaleDesktopFile(existing));
if (systemEntry) {
if (existing !== null && (existing.includes(GENERATED_MARKER) || isStaleDesktopFile(existing))) {
if (owned) {
return removeFile(filePath, 'Removed a user .desktop entry shadowing the system entry');
}
logger.debug('System-wide .desktop entry detected; skipping user-local copy', {systemEntry});
return false;
}
if (existing !== null && !existing.includes(GENERATED_MARKER) && !isStaleDesktopFile(existing)) {
if (existing !== null && !owned) {
logger.debug('Linux .desktop entry was hand-edited; leaving untouched', {filePath});
return false;
}
@@ -7,6 +7,7 @@ import {
getDesktopWindowBehaviorSettings,
} from '@electron/common/DesktopConfig';
import {
addLinuxHardwareVideoDecodeFeatures,
addLinuxHardwareVideoEncodeFeatures,
addWindowsHardwareVideoEncodeFeatures,
appendConfiguredChromiumSwitches,
@@ -53,6 +54,7 @@ export function applyPreReadyChromiumConfiguration(channel: BuildChannel, argv:
const disabledChromiumFeatures = new Set(BASE_DISABLED_CHROMIUM_FEATURES);
const enabledChromiumFeatures = new Set<string>();
if (!disableHardwareAcceleration) {
addLinuxHardwareVideoDecodeFeatures(enabledChromiumFeatures);
addLinuxHardwareVideoEncodeFeatures(enabledChromiumFeatures);
addWindowsHardwareVideoEncodeFeatures(enabledChromiumFeatures);
}
@@ -98,6 +98,17 @@ describe('renderTrayIcon', () => {
assert.deepEqual(pixel(bitmap, 32, 6, 16), [0xff, 0xff, 0xff, 0xff]);
});
test('keeps the Linux monochrome pixmap within 256 pixels', () => {
const result = renderTrayIcon(FakeNativeImage.fromBitmap(256), 'white', false, 'linux');
const [representation] = result.representations.values();
assert.equal(representation.width, 256);
assert.equal(representation.height, 256);
const bitmap = result.toBitmap();
assert.deepEqual(pixel(bitmap, 256, 128, 50), [0xff, 0xff, 0xff, 0xff]);
assert.deepEqual(pixel(bitmap, 256, 128, 128), [0, 0, 0, 0]);
assert.deepEqual(pixel(bitmap, 256, 20, 128), [0, 0, 0, 0]);
});
test('skips representations whose bitmap does not match their size', () => {
const base = FakeNativeImage.fromBitmap(16);
base.toBitmap = () => Buffer.alloc(3);
+38 -1
View File
@@ -18,6 +18,7 @@ const MIN_GLYPH_CONTRAST = 32;
const GLYPH_CUTOUT_START = 0.1;
const GLYPH_CUTOUT_END = 0.5;
const LINUX_MONOCHROME_CANVAS_RATIO = 22 / 16;
const LINUX_MAX_PIXMAP_SIZE = 256;
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
const DOT_BGR: Bgr = [0x00, 0x00, 0xdd];
const TEMPLATE_DOT_BGR: Bgr = [0x00, 0x00, 0x00];
@@ -149,6 +150,36 @@ function cropTrayGlyph(buffer: Buffer, width: number, height: number): TrayBitma
return {buffer: target, width: size, height: size};
}
function downscaleTrayBitmap(bitmap: TrayBitmap, size: number): TrayBitmap {
if (bitmap.width <= size) return bitmap;
const scale = bitmap.width / size;
const buffer = Buffer.alloc(size * size * 4);
const sums = [0, 0, 0, 0];
for (let y = 0; y < size; y++) {
const top = y * scale;
const bottom = top + scale;
for (let x = 0; x < size; x++) {
const left = x * scale;
const right = left + scale;
sums.fill(0);
for (let sy = Math.floor(top); sy < Math.min(bitmap.height, Math.ceil(bottom)); sy++) {
const weightY = Math.min(bottom, sy + 1) - Math.max(top, sy);
for (let sx = Math.floor(left); sx < Math.min(bitmap.width, Math.ceil(right)); sx++) {
const weight = weightY * (Math.min(right, sx + 1) - Math.max(left, sx));
const offset = (sy * bitmap.width + sx) * 4;
for (let channel = 0; channel < 4; channel++) {
sums[channel] += bitmap.buffer[offset + channel] * weight;
}
}
}
for (let channel = 0; channel < 4; channel++) {
buffer[(y * size + x) * 4 + channel] = Math.round(sums[channel] / (scale * scale));
}
}
}
return {buffer, width: size, height: size};
}
function addMonochromeTrayMargins(bitmap: TrayBitmap, size: number): TrayBitmap {
const inset = Math.floor((size - bitmap.width) / 2);
const buffer = Buffer.alloc(size * size * 4);
@@ -210,7 +241,13 @@ export function renderTrayIcon(
appearance === 'template' ? TEMPLATE_DOT_BGR : DOT_BGR,
);
}
if (linuxMonochrome) bitmap = addMonochromeTrayMargins(bitmap, Math.ceil(logicalCanvasSize * scaleFactor));
if (linuxMonochrome) {
const canvasSize = Math.min(Math.ceil(logicalCanvasSize * scaleFactor), LINUX_MAX_PIXMAP_SIZE);
bitmap = addMonochromeTrayMargins(
downscaleTrayBitmap(bitmap, Math.floor(canvasSize / LINUX_MONOCHROME_CANVAS_RATIO)),
canvasSize,
);
}
result.addRepresentation({scaleFactor, ...bitmap});
}
if (result.isEmpty()) {
@@ -1656,7 +1656,7 @@ Default `60000`. How often discovery is refetched, in milliseconds, and so the l
#### `FLUXER_APP_PROXY_SAME_ORIGIN_HOSTS`
Default empty. Hostnames, comma separated, without a scheme, port or path. When a request's `Host` is in the list, the page bootstrap points the web app and its API at that host, as `https://<host>` and `https://<host>/api`. Every other request gets the discovery values unchanged. Set it when the web app is served on more than one hostname and each hostname routes `/api` to the API. Invalid entries are logged and dropped.
Default empty. Hostnames, comma separated, without a scheme, port or path. When a request's `Host` is in the list, the page bootstrap points the web app and its API at that host, as `https://<host>` and `https://<host>/api`. The Gateway, media, upload relay and static CDN endpoints move to that host too when discovery puts them on the same host as the API, so `wss://chat.example.com/gateway` becomes `wss://<host>/gateway`. Endpoints on other hosts stay as they are. Every other request gets the discovery values unchanged. Set it when the web app is served on more than one hostname and each hostname routes `/api`, `/gateway` and `/media` like the main one. Invalid entries are logged and dropped.
#### `FLUXER_APP_PROXY_MANIFEST_SCOPE_EXTENSIONS`
@@ -1696,7 +1696,7 @@ Defaults to the crate version. The reported build of `admin` and `app-proxy`. `B
`FLUXER_CSP_EXTRA_DEFAULT_SRC`, `FLUXER_CSP_EXTRA_CONNECT_SRC`, `FLUXER_CSP_EXTRA_IMG_SRC`, `FLUXER_CSP_EXTRA_MEDIA_SRC`, `FLUXER_CSP_EXTRA_FONT_SRC`, `FLUXER_CSP_EXTRA_SCRIPT_SRC`, `FLUXER_CSP_EXTRA_STYLE_SRC`, `FLUXER_CSP_EXTRA_FRAME_SRC`, `FLUXER_CSP_EXTRA_WORKER_SRC`, and `FLUXER_CSP_EXTRA_MANIFEST_SRC` take one or more sources separated by commas, spaces, tabs, or newlines. Blank entries and sources the directive already lists are dropped. `FLUXER_CSP_REPORT_URI` sets a single `report-uri` value.
`object-src`, `base-uri`, and `frame-ancestors` are fixed and have no override. `app-proxy` adds the static CDN endpoint and the media endpoint from its environment. The self-hosted image also adds the origin of each branding image to `img-src`. At start it takes them from `FLUXER_APP_ICON_URL`, `FLUXER_APP_SYMBOL_URL`, `FLUXER_APP_LOGO_URL`, `FLUXER_APP_WORDMARK_URL` and `FLUXER_APP_FAVICON_URL`. It then polls the discovery document every `DISCOVERY_REFRESH_INTERVAL_MS`, where a value saved in the admin dashboard wins. A dashboard change reaches the header within a minute by default with no restart. A stack on one hostname needs no extra sources. The usual reason to set one is a voice server on another hostname, which needs its WebSocket origin in `FLUXER_CSP_EXTRA_CONNECT_SRC`. [Voice media does not use the proxy](/operator/reverse-proxy/#voice-media-does-not-use-the-proxy) has that line in place.
`object-src`, `base-uri`, and `frame-ancestors` are fixed and have no override. `app-proxy` adds the static CDN endpoint and the media endpoint from its environment. The self-hosted image also adds the origin of each branding image to `img-src`. At start it takes them from `FLUXER_APP_ICON_URL`, `FLUXER_APP_SYMBOL_URL`, `FLUXER_APP_LOGO_URL`, `FLUXER_APP_WORDMARK_URL` and `FLUXER_APP_FAVICON_URL`. It then polls the discovery document every `DISCOVERY_REFRESH_INTERVAL_MS`, where a value saved in the admin dashboard wins. A dashboard change reaches the header within a minute by default with no restart. From the same document it adds the origins of the API, gateway, media, upload relay and static CDN endpoints, so a web app served on a second hostname through `FLUXER_APP_ORIGIN_ALIASES` can still reach them. A stack on one hostname needs no extra sources. The usual reason to set one is a voice server on another hostname, which needs its WebSocket origin in `FLUXER_CSP_EXTRA_CONNECT_SRC`. [Voice media does not use the proxy](/operator/reverse-proxy/#voice-media-does-not-use-the-proxy) has that line in place.
A front proxy must not add a [Content-Security-Policy of its own](/operator/reverse-proxy/#send-no-content-security-policy-of-its-own).
@@ -58,6 +58,8 @@ If both answer, skip ahead. On a Mac or a Windows PC, install [Docker Desktop](h
Debian and Ubuntu need Docker's apt repository from [Install Docker Engine](https://docs.docker.com/engine/install/), because the distribution's own `docker.io` package ships no Compose plugin. Follow the [post-installation steps](https://docs.docker.com/engine/install/linux-postinstall/) to add your user to the `docker` group, then log out and back in so the commands below run without `sudo`.
Podman 5 works too, with `docker-compose` installed so `podman compose` has a provider. The installer picks Podman when `docker` is missing or does not answer. Podman logs every healthcheck run to the journal by default, so with Podman the installer writes `fluxer-healthcheck-events.conf` to `~/.config/containers/containers.conf.d/` (or `/etc/containers/containers.conf.d/` as root) to turn that off. Health checks still run. If you set `healthcheck_events` yourself, the installer leaves it alone.
Linux also needs `curl` and `openssl`, which the steps below use:
```bash
@@ -184,7 +186,7 @@ Leave `FLUXER_PUBLIC_ORIGIN` commented out, or keep it consistent with the publi
| `--update` | Upgrade: record, back up, refresh, pull, recreate, verify |
| `--rollback` | Restore the images and stack files that the newest `--update` run recorded |
| `--allow-root` | Permit running as root |
| `--engine <command>` | Container engine to drive. Default `docker`, or `podman` when `docker` is absent |
| `--engine <command>` | Container engine to drive. Default `docker`, or `podman` when `docker` is absent or does not answer |
Run `--dry-run` first to see what a set of flags does. The PowerShell script takes the same flags as `-Domain`, `-Email` and so on, and has no `-AllowRoot`. [Upgrading](/operator/upgrading/#run-the-upgrade) has the flags that only `--update` and `--rollback` read, and the exit codes.
+9 -1
View File
@@ -240,7 +240,7 @@ function Show-FluxerUsage {
Write-FluxerLine ' -Domain <host> Hostname the instance answers on. Prompted when absent.'
Write-FluxerLine ' -Email <address> Contact email for web push. Default: admin@<domain>.'
Write-FluxerLine ' -Engine <command> Container engine to drive. Default: docker, or podman when'
Write-FluxerLine ' docker is absent.'
Write-FluxerLine ' docker is absent or does not answer.'
Write-FluxerLine ' -Dir <path> Working directory. Default: the fluxer folder in the home'
Write-FluxerLine ' directory, or the working directory itself when -Update or'
Write-FluxerLine ' -Rollback is given and it holds an instance.'
@@ -385,6 +385,14 @@ function Invoke-FluxerPreflight {
$script:FluxerEngine = $Engine
} elseif ($null -ne (Get-Command docker -ErrorAction SilentlyContinue)) {
$script:FluxerEngine = 'docker'
if ($null -ne (Get-Command podman -ErrorAction SilentlyContinue) -and (Invoke-FluxerCapture @('info')).Code -ne 0) {
$script:FluxerEngine = 'podman'
if ((Invoke-FluxerCapture @('info')).Code -eq 0) {
Write-FluxerLine 'docker does not answer and podman does, so this run drives podman. Pass -Engine docker to use docker instead.'
} else {
$script:FluxerEngine = 'docker'
}
}
} elseif ($null -ne (Get-Command podman -ErrorAction SilentlyContinue)) {
$script:FluxerEngine = 'podman'
} else {
+43 -12
View File
@@ -214,7 +214,7 @@ Options:
--domain <host> Hostname the instance answers on. Prompted when absent.
--email <address> Contact email for web push. Default admin@<domain>.
--engine <command> Container engine to drive. Default docker, or podman
when docker is absent.
when docker is absent or does not answer.
--dir <path> Working directory. Default ~/fluxer, or the working
directory itself when --update or --rollback is given
and it holds an instance.
@@ -561,6 +561,10 @@ fluxer_resolve_engine() {
fluxer_engine=$opt_engine
elif command -v docker >/dev/null 2>&1; then
fluxer_engine='docker'
if command -v podman >/dev/null 2>&1 && ! docker info >/dev/null 2>&1 && podman info >/dev/null 2>&1; then
fluxer_engine='podman'
fluxer_say 'docker does not answer and podman does, so this run drives podman. Pass --engine docker to use docker instead.'
fi
elif command -v podman >/dev/null 2>&1; then
fluxer_engine='podman'
else
@@ -576,18 +580,11 @@ fluxer_preflight() {
fluxer_fail 2 'Running as root. Use an account in the docker group, or pass --allow-root.'
fi
fluxer_resolve_engine
if ! $fluxer_engine compose version >/dev/null 2>&1; then
fluxer_fail 2 "$fluxer_engine has no compose subcommand. $(fluxer_docker_hint)"
fi
if $fluxer_engine compose version 2>/dev/null | grep -q '^podman-compose version'; then
fluxer_fail 2 "$fluxer_engine compose runs podman-compose, which lacks compose ps -a and compose config --images that this script runs. $(fluxer_docker_hint)"
fi
fluxer_engine_report=$($fluxer_engine --version 2>/dev/null)
fluxer_engine_kind=$(printf '%s\n' "$fluxer_engine_report" | sed -n 's/^\([A-Za-z][A-Za-z]*\) version .*/\1/p' | tr 'A-Z' 'a-z')
fluxer_engine_version=$(printf '%s\n' "$fluxer_engine_report" | sed -n 's/^[A-Za-z][A-Za-z]* version v\{0,1\}\([0-9][0-9.]*\).*/\1/p')
fluxer_compose_version=$($fluxer_engine compose version --short 2>/dev/null | sed -n 's/^v\{0,1\}\([0-9][0-9.]*\).*/\1/p')
if [ -z "$fluxer_engine_version" ] || [ -z "$fluxer_compose_version" ]; then
fluxer_fail 2 "Cannot read the engine and Compose versions from $fluxer_engine. It answered \"$fluxer_engine_report\" to --version."
if [ -z "$fluxer_engine_version" ]; then
fluxer_fail 2 "Cannot read the engine version from $fluxer_engine. It answered \"$fluxer_engine_report\" to --version."
fi
case $fluxer_engine_kind in
podman)
@@ -600,7 +597,17 @@ fluxer_preflight() {
;;
esac
if ! fluxer_version_ge "$fluxer_engine_version" "$fluxer_min_engine"; then
fluxer_fail 2 "$fluxer_engine_label $fluxer_engine_version with Compose $fluxer_compose_version. Fluxer needs $fluxer_engine_label $fluxer_min_engine or newer."
fluxer_fail 2 "$fluxer_engine_label $fluxer_engine_version. Fluxer needs $fluxer_engine_label $fluxer_min_engine or newer. $(fluxer_docker_hint)"
fi
if ! $fluxer_engine compose version >/dev/null 2>&1; then
fluxer_fail 2 "$fluxer_engine has no compose subcommand. $(fluxer_docker_hint)"
fi
if $fluxer_engine compose version 2>/dev/null | grep -q '^podman-compose version'; then
fluxer_fail 2 "$fluxer_engine compose runs podman-compose, which lacks compose ps -a and compose config --images that this script runs. $(fluxer_docker_hint)"
fi
fluxer_compose_version=$($fluxer_engine compose version --short 2>/dev/null | sed -n 's/^v\{0,1\}\([0-9][0-9.]*\).*/\1/p')
if [ -z "$fluxer_compose_version" ]; then
fluxer_fail 2 "Cannot read the Compose version from $fluxer_engine compose version --short."
fi
if ! fluxer_version_ge "$fluxer_compose_version" "$FLUXER_MIN_COMPOSE"; then
fluxer_fail 2 "$fluxer_engine_label $fluxer_engine_version with Compose $fluxer_compose_version. Fluxer needs Compose $FLUXER_MIN_COMPOSE or newer."
@@ -837,7 +844,11 @@ fluxer_print_plan() {
fluxer_say " email admin@$opt_domain, derived by compose"
fluxer_plan_non_secret=$((fluxer_plan_non_secret - 1))
fi
fluxer_say ' action download the stack files, write .env, start the stack'
if [ "$opt_no_start" -eq 1 ]; then
fluxer_say ' action download the stack files, write .env, leave the stack stopped'
else
fluxer_say ' action download the stack files, write .env, start the stack'
fi
fluxer_say " .env keys $fluxer_plan_non_secret non-secret values and $(fluxer_secret_keys | wc -l | tr -d ' ') secrets"
fluxer_say ' files docker-compose.yml docker-compose.proxy.yml tunnel.compose.yml Caddyfile .env.example'
if [ -e "$opt_dir/.env" ]; then
@@ -2195,9 +2206,29 @@ fluxer_run_rollback() {
exit 0
}
# Podman writes a health_status event to the journal for every healthcheck run,
# with every label of the container in it, and this stack runs two dozen checks
# every ten seconds. Compose and this script read health from inspect, so the
# events have no reader here. An operator who set the key either way keeps it.
fluxer_quiet_podman_health_events() {
[ "$fluxer_engine_kind" = 'podman' ] || return 0
[ "$opt_dry_run" -eq 0 ] || return 0
if [ "$(id -u)" -eq 0 ]; then
fluxer_conf_dir=/etc/containers/containers.conf.d
else
fluxer_conf_dir=${XDG_CONFIG_HOME:-$HOME/.config}/containers/containers.conf.d
fi
if grep -qs '^[[:space:]]*healthcheck_events[[:space:]]*=' /usr/share/containers/containers.conf /etc/containers/containers.conf /etc/containers/containers.conf.d/*.conf "${XDG_CONFIG_HOME:-$HOME/.config}/containers/containers.conf" "${XDG_CONFIG_HOME:-$HOME/.config}/containers/containers.conf.d/"*.conf; then
return 0
fi
mkdir -p "$fluxer_conf_dir" && printf '[engine]\nhealthcheck_events = false\n' > "$fluxer_conf_dir/fluxer-healthcheck-events.conf" || return 0
fluxer_say "Wrote $fluxer_conf_dir/fluxer-healthcheck-events.conf, which stops Podman from logging every healthcheck run to the journal. Health checks still run."
}
fluxer_preflight
fluxer_validate_options
fluxer_resolve_values
fluxer_quiet_podman_health_events
if [ "$opt_update" -eq 1 ]; then
fluxer_refresh_installer
+112 -15
View File
@@ -9,7 +9,7 @@ use crate::metrics::{JobKind, JobRejection, Provider, elapsed_ms};
use crate::payload;
use crate::providers::{self, SendOutcome};
use crate::retry::{self, RETRY_DEADLINE};
use crate::rpc::RpcError;
use crate::rpc::{RpcClient, RpcError};
use crate::server::AppState;
use crate::subscription::{Platform, Subscription};
use crate::unix_millis;
@@ -353,13 +353,7 @@ async fn run_message_job(
) -> anyhow::Result<()> {
let started_ms = now_ms();
let deadline = Instant::now() + RETRY_DEADLINE;
let (badges, subscriptions) = lookup(deadline, || async {
tokio::try_join!(
state.rpc.badge_counts(&job.user_ids),
state.rpc.push_subscriptions(&job.user_ids)
)
})
.await?;
let (badges, subscriptions) = lookup_recipients(&state.rpc, &job.user_ids, deadline).await?;
state.metrics.record_recipients(job.user_ids.len() as u64);
let envelopes = job
@@ -410,13 +404,7 @@ async fn run_clear_job(state: &AppState, sends: &Semaphore, job: ClearJob) -> an
let started_ms = now_ms();
let deadline = Instant::now() + RETRY_DEADLINE;
let user_ids = std::slice::from_ref(&job.user_id);
let (badges, subscriptions) = lookup(deadline, || async {
tokio::try_join!(
state.rpc.badge_counts(user_ids),
state.rpc.push_subscriptions(user_ids)
)
})
.await?;
let (badges, subscriptions) = lookup_recipients(&state.rpc, user_ids, deadline).await?;
state.metrics.record_recipients(1);
let envelope = payload::web_push_clear(&job, badge_of(&badges, &job.user_id));
@@ -503,6 +491,24 @@ async fn run_ring_job(state: &AppState, sends: &Semaphore, job: RingJob) -> anyh
Ok(())
}
async fn lookup_recipients(
rpc: &RpcClient,
user_ids: &[String],
deadline: Instant,
) -> Result<(HashMap<String, u32>, HashMap<String, Vec<Subscription>>), RpcError> {
let subscriptions = lookup(deadline, || rpc.push_subscriptions(user_ids)).await?;
let subscribed_ids: Vec<String> = user_ids
.iter()
.filter(|user_id| subscribed(&subscriptions, user_id))
.cloned()
.collect();
if subscribed_ids.is_empty() {
return Ok((HashMap::new(), subscriptions));
}
let badges = lookup(deadline, || rpc.badge_counts(&subscribed_ids)).await?;
Ok((badges, subscriptions))
}
async fn lookup<T, F, Fut>(deadline: Instant, mut call: F) -> Result<T, RpcError>
where
F: FnMut() -> Fut,
@@ -708,4 +714,95 @@ mod tests {
assert!(Audience::Standard.admits(&subscription("ios_apns", false)));
assert!(Audience::Standard.admits(&subscription("ios_apns", true)));
}
async fn fake_api(
subscribed: &'static [&'static str],
) -> (RpcClient, Arc<std::sync::Mutex<Vec<Value>>>) {
let calls = Arc::new(std::sync::Mutex::new(Vec::new()));
let recorded = Arc::clone(&calls);
let app = axum::Router::new().route(
"/",
axum::routing::post(move |body: axum::body::Bytes| {
let recorded = Arc::clone(&recorded);
async move {
let request: Value = serde_json::from_slice(&body).unwrap();
recorded.lock().unwrap().push(request.clone());
let user_ids = request["user_ids"].as_array().cloned().unwrap_or_default();
let data = match request["type"].as_str().unwrap() {
"get_push_subscriptions" => serde_json::json!(
subscribed
.iter()
.map(|user_id| {
(
(*user_id).to_owned(),
serde_json::json!([{
"subscription_id": "sub",
"endpoint": "token",
"p256dh_key": null,
"auth_key": null,
"platform": "android_fcm",
"app_id": null,
"provider_environment": null
}]),
)
})
.collect::<serde_json::Map<_, _>>()
),
"get_badge_counts" => serde_json::json!({
"badge_counts": user_ids
.iter()
.map(|user_id| (user_id.as_str().unwrap().to_owned(), serde_json::json!(3)))
.collect::<serde_json::Map<_, _>>()
}),
other => panic!("unexpected rpc {other}"),
};
serde_json::json!({ "data": data }).to_string()
}
}),
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let url = format!("http://{}/", listener.local_addr().unwrap());
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
let rpc = RpcClient::new(
&crate::config::RpcConfig {
url,
auth_token: fluxer_common::secret::SecretString::new("token".to_owned()),
},
reqwest::Client::new(),
Arc::new(crate::metrics::Metrics::new()),
);
(rpc, calls)
}
fn badge_requests(calls: &std::sync::Mutex<Vec<Value>>) -> Vec<Value> {
calls
.lock()
.unwrap()
.iter()
.filter(|call| call["type"] == "get_badge_counts")
.map(|call| call["user_ids"].clone())
.collect()
}
#[tokio::test]
async fn badge_counts_are_read_only_for_recipients_with_a_device() {
let (rpc, calls) = fake_api(&["2"]).await;
let user_ids: Vec<String> = (1..=500).map(|id| id.to_string()).collect();
let deadline = Instant::now() + Duration::from_secs(5);
let (badges, subscriptions) = lookup_recipients(&rpc, &user_ids, deadline).await.unwrap();
assert_eq!(badge_requests(&calls), vec![serde_json::json!(["2"])]);
assert_eq!(badges, HashMap::from([("2".to_owned(), 3)]));
assert!(subscribed(&subscriptions, "2"));
}
#[tokio::test]
async fn no_badge_counts_are_read_when_nobody_has_a_device() {
let (rpc, calls) = fake_api(&[]).await;
let user_ids: Vec<String> = (1..=500).map(|id| id.to_string()).collect();
let deadline = Instant::now() + Duration::from_secs(5);
let (badges, _subscriptions) = lookup_recipients(&rpc, &user_ids, deadline).await.unwrap();
assert!(badge_requests(&calls).is_empty());
assert!(badges.is_empty());
assert_eq!(calls.lock().unwrap().len(), 1);
}
}
@@ -329,17 +329,53 @@ function withEndpointsOnServingOrigin(url: string, document: InstanceDiscoveryDo
return document;
}
const api = `${servedBy}${INSTANCE_DISCOVERY_CONVENTIONAL_ENDPOINT_PATH}`;
const declaredApiHost = hostOfEndpoint(document.endpoints.api);
const moveToServingOrigin = <T extends string | null>(endpoint: T): T =>
(endpoint == null ? endpoint : (onServingOrigin(endpoint, declaredApiHost, servedBy) ?? endpoint)) as T;
return {
...document,
endpoints: {
...document.endpoints,
api,
api_client: document.endpoints.api_client == null ? null : api,
gateway: moveToServingOrigin(document.endpoints.gateway),
media: moveToServingOrigin(document.endpoints.media),
upload_relay: moveToServingOrigin(document.endpoints.upload_relay),
static_cdn: moveToServingOrigin(document.endpoints.static_cdn),
webapp: document.endpoints.webapp == null ? null : servedBy,
},
};
}
function hostOfEndpoint(value: string): string | null {
try {
return new URL(value).host;
} catch {
return null;
}
}
function onServingOrigin(endpoint: string, declaredApiHost: string | null, servedBy: string): string | null {
if (declaredApiHost == null) {
return null;
}
let url: URL;
let serving: URL;
try {
url = new URL(endpoint);
serving = new URL(servedBy);
} catch {
return null;
}
if (url.host !== declaredApiHost) {
return null;
}
const secure = serving.protocol === 'https:';
const socket = url.protocol === 'ws:' || url.protocol === 'wss:';
const path = url.pathname === '/' && !endpoint.endsWith('/') ? '' : url.pathname;
return `${socket ? (secure ? 'wss:' : 'ws:') : serving.protocol}//${serving.host}${path}${url.search}`;
}
export function requireDiscoveryDeclaresItsOwnOrigin(url: string, document: InstanceDiscoveryDocument): void {
const servedBy = originOfEndpoint(url);
if (servedBy == null) {
@@ -951,14 +951,12 @@ const LimitRuleSchema = z.object({
filters: LimitFilterSchema.optional().describe('Optional filters that scope the rule'),
limits: z
.record(z.string(), NonNegativeSafeIntegerType)
.refine(
(limits) => {
const limitKeys = Object.keys(limits);
return limitKeys.every((key) => (LIMIT_KEYS as ReadonlyArray<string>).includes(key));
},
{error: 'Invalid limit key detected'},
)
.superRefine((limits, ctx) => {
for (const key of Object.keys(limits)) {
if (!(LIMIT_KEYS as ReadonlyArray<string>).includes(key)) {
ctx.addIssue({code: 'custom', message: ValidationErrorCodes.INVALID_FORMAT, path: [key]});
}
}
for (const key of LIMIT_KEYS) {
const value = limits[key];
const {min = 0, max} = LIMIT_KEY_METADATA[key];