feat: enable push notifications for several accounts at once

The notification panel only ever addresses the account on screen, so
arming a browser for five accounts meant switching account five times
and answering the same permission prompt on each pass.

The browser holds a single push subscription: enabling an account
registers it against that one subscription rather than creating another.
A section in the notification settings now lists every connected account
with its current state and turns push on or off for the ones selected,
walking them against the same subscription.

A refusal at the browser level - unsupported, or permission denied -
applies to every account, so it stops the walk instead of failing each
account in turn and prompting repeatedly.
This commit is contained in:
lucamzanon
2026-09-19 05:44:45 +02:00
parent fdfcb1711e
commit e468b08fb7
31 changed files with 527 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { SettingsSection } from './settings-section';
import { useAccountStore } from '@/stores/account-store';
import { useAuthStore } from '@/stores/auth-store';
import { disableWebPush, enableWebPushForAccounts, isWebPushEnabled, isWebPushSupported } from '@/lib/web-push';
/**
* Turn push on or off for several accounts at once.
*
* The per-account panel above only ever addresses the account on screen, so
* arming a fresh device used to mean switching account N times. One browser
* subscription already serves every account (see `enableWebPushForAccounts`);
* what was missing was a place to register them all against it in one go.
*/
export function MultiAccountPushSettings({ relayBaseUrl }: { relayBaseUrl?: string }) {
const t = useTranslations('settings.notifications.push_all');
const accounts = useAccountStore(s => s.accounts);
// Re-read the client map whenever a connection appears or drops.
const revision = useAuthStore(s => s.connectedAccountsRevision);
const [enabled, setEnabled] = useState<Record<string, boolean>>({});
const [chosen, setChosen] = useState<Set<string>>(new Set());
const [busy, setBusy] = useState(false);
const [notice, setNotice] = useState('');
const connected = accounts.filter(a => a.isConnected && useAuthStore.getState().getClientForAccount(a.id));
const refresh = useCallback(async () => {
const entries = await Promise.all(
useAccountStore.getState().accounts.map(async a => [a.id, await isWebPushEnabled(a.id)] as const),
);
setEnabled(Object.fromEntries(entries));
}, []);
useEffect(() => { void refresh(); }, [refresh, revision]);
if (!isWebPushSupported() || connected.length < 2) return null;
const toggle = (id: string, on: boolean) => setChosen(prev => {
const next = new Set(prev);
if (on) next.add(id); else next.delete(id);
return next;
});
const run = async (action: 'enable' | 'disable') => {
const auth = useAuthStore.getState();
const targets = connected.filter(a => chosen.has(a.id));
if (!targets.length) return;
setBusy(true); setNotice('');
try {
if (action === 'enable') {
const outcome = await enableWebPushForAccounts(
targets.map(a => ({ accountId: a.id, client: auth.getClientForAccount(a.id)!, accountLabel: a.username })),
{ relayBaseUrl },
);
setNotice(outcome.failed.length
? t('result_partial', { enabled: outcome.enabled.length, failed: outcome.failed.length })
: t('result_enabled', { count: outcome.enabled.length }));
} else {
// Teardown swallows its own failures, so every account ends up local-off.
for (const account of targets) {
await disableWebPush({ client: auth.getClientForAccount(account.id)!, relayBaseUrl });
}
setNotice(t('result_disabled', { count: targets.length }));
}
setChosen(new Set());
} finally {
setBusy(false);
await refresh();
}
};
const allChosen = connected.length > 0 && connected.every(a => chosen.has(a.id));
return <SettingsSection title={t('title')} description={t('description')}>
<div className="space-y-3">
<label className="flex items-center gap-2 text-sm font-medium">
<input type="checkbox" disabled={busy} checked={allChosen}
onChange={e => setChosen(e.target.checked ? new Set(connected.map(a => a.id)) : new Set())} />
{t('select_all')}
</label>
<ul className="space-y-2">
{connected.map(account => <li key={account.id}>
<label className="flex items-start gap-2 pl-6 text-sm">
<input type="checkbox" className="mt-1" disabled={busy} checked={chosen.has(account.id)}
onChange={e => toggle(account.id, e.target.checked)} />
<span>
<span className="block">{account.displayName || account.label || account.username}</span>
<span className="block text-xs text-muted-foreground">
{account.email || account.username} · {enabled[account.id] ? t('state_on') : t('state_off')}
</span>
</span>
</label>
</li>)}
</ul>
<div className="flex flex-wrap items-center gap-2">
<Button type="button" onClick={() => void run('enable')} disabled={busy || !chosen.size}>
{busy && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}{t('enable_selected')}
</Button>
<Button type="button" variant="outline" onClick={() => void run('disable')} disabled={busy || !chosen.size}>
{t('disable_selected')}
</Button>
</div>
{notice && <p className="text-sm text-muted-foreground">{notice}</p>}
</div>
</SettingsSection>;
}
@@ -11,6 +11,7 @@ import { Loader2, RefreshCw, Volume2, XCircle } from 'lucide-react';
import { usePolicyStore } from '@/stores/policy-store';
import { useAuthStore } from '@/stores/auth-store';
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
import { MultiAccountPushSettings } from './multi-account-push';
import { useConfirmDialog } from '@/hooks/use-confirm-dialog';
import {
WebPushUnsupportedError,
@@ -251,6 +252,8 @@ export function NotificationSettings() {
)}
</SettingsSection>
<MultiAccountPushSettings relayBaseUrl={activeRelayUrl} />
{client && (
<SettingsSection title={t('push.devices_title')} description={t('push.devices_desc')}>
<div className="flex items-center justify-between gap-2">
+38
View File
@@ -4,6 +4,7 @@ import type { EmailPushConfig, PushSubscription as JmapPushSubscription } from '
import {
disableWebPush,
enableWebPush,
enableWebPushForAccounts,
listPushDevices,
resetWebPushResyncState,
resyncWebPush,
@@ -454,3 +455,40 @@ describe('revokePushDevice', () => {
expect(localStorage.getItem(SUB_KEY)).toBeNull();
});
});
describe('enableWebPushForAccounts', () => {
it('registers every account against the one browser subscription', async () => {
const first = makeClient([]);
const second = makeClient([]);
installFetch({});
const result = await enableWebPushForAccounts([
{ accountId: 'a', client: first, accountLabel: 'one' },
{ accountId: 'b', client: second, accountLabel: 'two' },
], { relayBaseUrl: RELAY });
expect(result.enabled).toEqual(['a', 'b']);
expect(result.failed).toEqual([]);
expect(first.createPushSubscription).toHaveBeenCalledTimes(1);
expect(second.createPushSubscription).toHaveBeenCalledTimes(1);
});
it('stops at a browser-level refusal instead of prompting once per account', async () => {
const first = makeClient([]);
const second = makeClient([]);
installFetch({});
// Blocked notifications are a property of the browser, not of an account:
// walking on would fail identically for every one of them.
(globalThis as unknown as { Notification: unknown }).Notification = { permission: 'denied' };
(window as unknown as { Notification: unknown }).Notification = { permission: 'denied' };
const result = await enableWebPushForAccounts([
{ accountId: 'a', client: first },
{ accountId: 'b', client: second },
], { relayBaseUrl: RELAY });
expect(result.enabled).toEqual([]);
expect(result.failed.map(f => f.accountId)).toEqual(['a', 'b']);
expect(second.createPushSubscription).not.toHaveBeenCalled();
});
});
+51
View File
@@ -695,3 +695,54 @@ export async function resyncWebPush(params: ResyncWebPushParams): Promise<boolea
export function resetWebPushResyncState(): void {
resyncedAccountIds.clear();
}
export interface BulkWebPushTarget {
accountId: string;
client: IJMAPClient;
accountLabel?: string;
}
export interface BulkWebPushResult {
enabled: string[];
/** Accounts left untouched, with the reason the first failure gave. */
failed: Array<{ accountId: string; error: unknown }>;
}
/**
* Enable push for several accounts against one browser subscription.
*
* The browser only ever holds a single PushSubscription: `enableWebPush` reuses
* it and registers one server-side subscription per account, which is why every
* account has to be walked separately. Sequential on purpose - the first call
* is the one that may prompt for permission, and parallel prompts race.
*
* A browser-level refusal (unsupported, permission denied or dismissed) applies
* to every account, so it stops the walk instead of failing them one by one.
*/
export async function enableWebPushForAccounts(
targets: BulkWebPushTarget[],
options: { relayBaseUrl?: string; forceRecreate?: boolean } = {},
): Promise<BulkWebPushResult> {
const result: BulkWebPushResult = { enabled: [], failed: [] };
let browserRefusal: unknown;
for (const target of targets) {
if (browserRefusal !== undefined) {
result.failed.push({ accountId: target.accountId, error: browserRefusal });
continue;
}
try {
await enableWebPush({
client: target.client,
relayBaseUrl: options.relayBaseUrl,
accountLabel: target.accountLabel,
forceRecreate: options.forceRecreate,
});
result.enabled.push(target.accountId);
} catch (error) {
if (error instanceof WebPushUnsupportedError || !isWebPushSupported()
|| Notification.permission !== 'granted') browserRefusal = error;
result.failed.push({ accountId: target.accountId, error });
}
}
return result;
}
+12
View File
@@ -1183,6 +1183,18 @@
"confirm_revoke_message": "سيتوقّف فورًا عن تلقّي الإشعارات في الخلفية. تفعيل الدفع مجددًا على ذلك الجهاز سيسجّله من جديد.",
"confirm_revoke_message_this": "سيتوقّف هذا الجهاز فورًا عن تلقّي الإشعارات في الخلفية."
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "صوت الإشعار",
"description": "اختر الصوت الذي يُشغَّل للإشعارات",
+12
View File
@@ -1183,6 +1183,18 @@
"confirm_revoke_message": "Deixarà de rebre notificacions en segon pla immediatament. Si hi tornes a activar les notificacions push, es registrarà de nou.",
"confirm_revoke_message_this": "Aquest dispositiu deixarà de rebre notificacions en segon pla immediatament."
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "So de notificació",
"description": "Trieu quin so es reprodueix per a les notificacions",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "Rychlý",
"relax": "Relaxační"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Zvuk oznámení",
"description": "Vyberte zvuk, který se přehraje při oznámení",
+12
View File
@@ -1183,6 +1183,18 @@
"confirm_revoke_message": "Den stopper med at modtage baggrundsnotifikationer med det samme. Slår du push til igen på enheden, registreres den på ny.",
"confirm_revoke_message_this": "Denne enhed stopper med at modtage baggrundsnotifikationer med det samme."
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Notifikationslyd",
"description": "Vælg hvilken lyd der skal afspilles for notifikationer",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "Schnelle Geste",
"relax": "Entspannt"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Benachrichtigungston",
"description": "Wählen Sie den Ton für Benachrichtigungen",
+12
View File
@@ -1183,6 +1183,18 @@
"confirm_revoke_message": "It will stop receiving background notifications immediately. Enabling push again on that device registers it anew.",
"confirm_revoke_message_this": "This device will stop receiving background notifications immediately."
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Notification Sound",
"description": "Choose which sound to play for notifications",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "Gesto rápido",
"relax": "Relajado"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Sonido de notificación",
"description": "Elige qué sonido reproducir para las notificaciones",
+12
View File
@@ -1183,6 +1183,18 @@
"confirm_revoke_message": "بی‌درنگ دریافت اعلان‌های پس‌زمینه را متوقف می‌کند. با روشن‌کردن دوبارهٔ پوش روی آن دستگاه، دوباره ثبت می‌شود.",
"confirm_revoke_message_this": "این دستگاه بی‌درنگ دریافت اعلان‌های پس‌زمینه را متوقف می‌کند."
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "صدای اعلان",
"description": "صدای پخش شده برای اعلان‌ها",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "Geste rapide",
"relax": "Détente"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Son de notification",
"description": "Choisissez le son à jouer pour les notifications",
+12
View File
@@ -1109,6 +1109,18 @@
"swift": "מחווה מהירה",
"relax": "תירגע"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "צליל התראה",
"description": "בחר איזה צליל להפעיל עבור התראות",
+12
View File
@@ -1183,6 +1183,18 @@
"confirm_revoke_message": "Azonnal nem kap több háttérértesítést. Ha az eszközön újra bekapcsolja a push értesítéseket, újra regisztrálódik.",
"confirm_revoke_message_this": "Ez az eszköz azonnal nem kap több háttérértesítést."
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Értesítési hang",
"description": "Válaszd ki az értesítésekhez használt hangot",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "Gesto veloce",
"relax": "Rilassante"
},
"push_all": {
"title": "Tutti gli account",
"description": "Attiva o disattiva le notifiche per più account in una volta sola. Una sola iscrizione del browser li serve tutti.",
"select_all": "Seleziona tutti gli account collegati",
"state_on": "notifiche attive",
"state_off": "notifiche non attive",
"enable_selected": "Attiva per i selezionati",
"disable_selected": "Disattiva per i selezionati",
"result_enabled": "Notifiche attivate per {count} account.",
"result_partial": "Attivate per {enabled} account; {failed} non è stato possibile attivarle.",
"result_disabled": "Notifiche disattivate per {count} account."
},
"sound_selection": {
"title": "Suono di notifica",
"description": "Scegli quale suono riprodurre per le notifiche",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "スウィフトジェスチャー",
"relax": "リラックス"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "通知音",
"description": "通知に使用する音を選択",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "빠르게",
"relax": "차분하게"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "알림음",
"description": "알림이 올 때 재생할 소리를 선택해 주세요",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "Ātrs",
"relax": "Relaksējošs"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Paziņojuma skaņa",
"description": "Izvēlieties skaņu paziņojumiem",
+12
View File
@@ -1183,6 +1183,18 @@
"confirm_revoke_message": "Тэр даруй араарх мэдэгдэл хүлээн авахаа болино. Тухайн төхөөрөмж дээр push-ыг дахин асаавал дахин бүртгэгднэ.",
"confirm_revoke_message_this": "Энэ төхөөрөмж тэр даруй араарх мэдэгдэл хүлээн авахаа болино."
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Мэдэгдэлийн дуу",
"description": "Мэдэгдэл авахын тулд ямар дууг тоглуулахаа сонгоно уу",
+12
View File
@@ -1183,6 +1183,18 @@
"confirm_revoke_message": "Enheten slutter umiddelbart å motta bakgrunnsvarsler. Hvis pushvarsler aktiveres på nytt på enheten, registreres den på nytt.",
"confirm_revoke_message_this": "Denne enheten slutter umiddelbart å motta bakgrunnsvarsler."
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Varslingslyd",
"description": "Velg hvilken lyd som skal spilles av for varsler",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "Snel gebaar",
"relax": "Ontspannen"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Meldingsgeluid",
"description": "Kies welk geluid wordt afgespeeld voor meldingen",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "Szybki gest",
"relax": "Relaks"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Dźwięk powiadomienia",
"description": "Wybierz dźwięk odtwarzany dla powiadomień",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "Gesto rápido",
"relax": "Relaxante"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Som de notificação",
"description": "Escolha qual som reproduzir para notificações",
+12
View File
@@ -1183,6 +1183,18 @@
"confirm_revoke_message": "Va înceta imediat să primească notificări în fundal. Reactivarea push pe acel dispozitiv îl va înregistra din nou.",
"confirm_revoke_message_this": "Acest dispozitiv va înceta imediat să primească notificări în fundal."
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Sunet de notificare",
"description": "Alegeți ce sunet să se redea pentru notificări",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "Быстрый жест",
"relax": "Релакс"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Звук уведомления",
"description": "Выберите звук для уведомлений",
+12
View File
@@ -1147,6 +1147,18 @@
"swift": "Rýchly",
"relax": "Relaxačný"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Zvuk oznámenia",
"description": "Vyberte zvuk, ktorý sa prehrá pri oznámení",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "Hızlı Jest",
"relax": "Rahatla"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Bildirim Sesi",
"description": "Bildirimler için hangi sesin çalınacağını seçin",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "Швидкий жест",
"relax": "Розслабтеся"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "Звук сповіщення",
"description": "Виберіть звук для сповіщень",
+12
View File
@@ -1183,6 +1183,18 @@
"confirm_revoke_message": "此裝置會立即停止接收背景通知。若在該裝置上再次啟用推送,將重新註冊。",
"confirm_revoke_message_this": "此裝置會立即停止接收背景通知。"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "通知聲音",
"description": "選擇通知時播放的聲音",
+12
View File
@@ -1144,6 +1144,18 @@
"swift": "轻快手势",
"relax": "放松"
},
"push_all": {
"title": "All accounts",
"description": "Turn push on or off for several accounts at once. One browser subscription serves them all.",
"select_all": "Select every connected account",
"state_on": "notifications on",
"state_off": "notifications off",
"enable_selected": "Enable for selected",
"disable_selected": "Disable for selected",
"result_enabled": "Notifications enabled for {count} accounts.",
"result_partial": "Enabled for {enabled} accounts; {failed} could not be enabled.",
"result_disabled": "Notifications disabled for {count} accounts."
},
"sound_selection": {
"title": "通知声音",
"description": "选择通知时播放的声音",