mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
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:
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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": "اختر الصوت الذي يُشغَّل للإشعارات",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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í",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "صدای پخش شده برای اعلانها",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "בחר איזה צליל להפעיל עבור התראות",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "通知に使用する音を選択",
|
||||
|
||||
@@ -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": "알림이 올 때 재생할 소리를 선택해 주세요",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Мэдэгдэл авахын тулд ямар дууг тоглуулахаа сонгоно уу",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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ń",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Выберите звук для уведомлений",
|
||||
|
||||
@@ -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í",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Виберіть звук для сповіщень",
|
||||
|
||||
@@ -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": "選擇通知時播放的聲音",
|
||||
|
||||
@@ -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": "选择通知时播放的声音",
|
||||
|
||||
Reference in New Issue
Block a user