diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
14 files changed, 224 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/app.js b/packages/meshbay-hub/src/meshbay_hub/static/app.js index b9466d0..2254a11 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/app.js @@ -31,6 +31,7 @@ import { } from './playlists.js'; import { IndexingDock } from './index-dock.js'; import { SettingsPage } from './settings-page.js'; +import { syncPush, disablePush } from './push.js'; import { ProfilePage } from './profile-page.js'; import { ExplorePage } from './explore-page.js'; import { GroupName } from './group-name.js'; @@ -1042,6 +1043,7 @@ function App() { .catch(() => {}); refreshNodeKey(); fetchNotifications(); + syncPush(user).catch(() => {}); }, [user]); // The node this application ships, set up, started and linked for whoever @@ -1233,6 +1235,13 @@ function App() { // Navigating away leaves transfers running; signing out does not. They // are moving data on tokens that are about to stop being ours. transfers.reset(); + // This phone stops being told about the account it is leaving. Its + // access token is still good for that request; the refresh token's + // revocation below does not touch it. + // The stored session, read now: React's copy can be a renewal behind, + // and once the session is cleared nothing could renew it. + disablePush(user && { ...user, token: (loadAuth() || {}).token || user.token }) + .catch(() => {}); // Revoked on the hub too, so a copy of the refresh token is worth // nothing. Read before the next line clears it. logoutOnHub(); diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index 4dff605..d8b7b5c 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -436,6 +436,15 @@ export default { 'settings.email_verified': 'E-Mail-Adresse erfolgreich geändert.', 'settings.notif_global_disable': 'Disable all notifications', 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.', + 'settings.push_label': 'Benachrichtigungen auf diesem Telefon', + 'settings.push_off': 'Aus. Einschalten, um bei geschlossenem MeshBay über neue Nachrichten und Einladungen informiert zu werden.', + 'settings.push_pending': 'Warte auf die Antwort des Verteilers…', + 'settings.push_blocked': 'Android blockiert die Benachrichtigungen von MeshBay: Erlauben Sie sie in den Systemeinstellungen.', + 'settings.push_ready': 'An. Neue Benachrichtigungen erreichen dieses Telefon über den Verteiler, verschlüsselt, sodass er sie nicht lesen kann.', + 'settings.push_poll': 'An. Dieses Telefon sieht etwa alle fünfzehn Minuten nach neuen Benachrichtigungen. Damit sie sofort ankommen, können Sie eine UnifiedPush-Verteiler-App wie ntfy installieren (optional).', + 'settings.push_global_off': 'Solange alle Benachrichtigungen ausgeschaltet sind, wird nichts gesendet.', + 'push.channel_chat': 'Nachrichten', + 'push.channel_other': 'Einladungen und Konto', 'settings.defaults': 'Standardwerte', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 90c043e..a4a8215 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -433,6 +433,15 @@ export default { 'settings.email_verified': 'Email address changed successfully.', 'settings.notif_global_disable': 'Disable all notifications', 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.', + 'settings.push_label': 'Notifications on this phone', + 'settings.push_off': 'Off. Turn on to be told about new messages and invitations while MeshBay is closed.', + 'settings.push_pending': 'Waiting for the distributor’s answer…', + 'settings.push_blocked': 'Android is blocking MeshBay’s notifications: allow them in the system settings.', + 'settings.push_ready': 'On. New notifications reach this phone through the distributor, encrypted so it cannot read them.', + 'settings.push_poll': 'On. This phone checks for new notifications about every fifteen minutes. For them to arrive at once, you can install a UnifiedPush distributor app such as ntfy (optional).', + 'settings.push_global_off': 'Nothing is sent while every notification is turned off.', + 'push.channel_chat': 'Messages', + 'push.channel_other': 'Invitations and account', 'settings.defaults': 'Defaults', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 7b46b17..78160d4 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -434,6 +434,15 @@ export default { 'settings.email_verified': 'Dirección de correo cambiada con éxito.', 'settings.notif_global_disable': 'Disable all notifications', 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.', + 'settings.push_label': 'Notificaciones en este teléfono', + 'settings.push_off': 'Desactivadas. Actívalas para enterarte de nuevos mensajes e invitaciones con MeshBay cerrado.', + 'settings.push_pending': 'Esperando la respuesta del distribuidor…', + 'settings.push_blocked': 'Android bloquea las notificaciones de MeshBay: permítelas en los ajustes del sistema.', + 'settings.push_ready': 'Activadas. Las nuevas notificaciones llegan a este teléfono a través del distribuidor, cifradas para que no pueda leerlas.', + 'settings.push_poll': 'Activadas. Este teléfono busca notificaciones nuevas cada quince minutos aproximadamente. Para recibirlas al instante, puedes instalar una app distribuidora de UnifiedPush como ntfy (opcional).', + 'settings.push_global_off': 'No se envía nada mientras todas las notificaciones estén desactivadas.', + 'push.channel_chat': 'Mensajes', + 'push.channel_other': 'Invitaciones y cuenta', 'settings.defaults': 'Valores predeterminados', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index d3f9200..1fbb6cf 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -435,6 +435,15 @@ export default { 'settings.email_verified': 'Adresse e-mail modifiée avec succès.', 'settings.notif_global_disable': 'Désactiver toutes les notifications', 'settings.notif_global_hint': 'Quand activé, aucune notification n\x27est créée pour aucun groupe.', + 'settings.push_label': 'Notifications sur ce téléphone', + 'settings.push_off': 'Désactivées. Activez-les pour être prévenu des nouveaux messages et invitations quand MeshBay est fermé.', + 'settings.push_pending': 'En attente de la réponse du distributeur…', + 'settings.push_blocked': 'Android bloque les notifications de MeshBay : autorisez-les dans les paramètres du système.', + 'settings.push_ready': 'Activées. Les nouvelles notifications arrivent sur ce téléphone par le distributeur, chiffrées pour qu’il ne puisse pas les lire.', + 'settings.push_poll': 'Activées. Ce téléphone vérifie les nouvelles notifications environ toutes les quinze minutes. Pour les recevoir immédiatement, vous pouvez installer une application de distribution UnifiedPush comme ntfy (facultatif).', + 'settings.push_global_off': 'Rien n’est envoyé tant que toutes les notifications sont désactivées.', + 'push.channel_chat': 'Messages', + 'push.channel_other': 'Invitations et compte', 'settings.defaults': 'Valeurs par défaut', 'settings.default_tab': 'Onglet par défaut', 'settings.default_tab_hint': 'L\'onglet qui s\'ouvre en premier quand vous entrez dans un groupe.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index 715738e..250e769 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -435,6 +435,15 @@ export default { 'settings.email_verified': 'Indirizzo e-mail modificato con successo.', 'settings.notif_global_disable': 'Disable all notifications', 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.', + 'settings.push_label': 'Notifiche su questo telefono', + 'settings.push_off': 'Disattivate. Attivale per essere avvisato di nuovi messaggi e inviti quando MeshBay è chiuso.', + 'settings.push_pending': 'In attesa della risposta del distributore…', + 'settings.push_blocked': 'Android blocca le notifiche di MeshBay: consentile nelle impostazioni di sistema.', + 'settings.push_ready': 'Attivate. Le nuove notifiche arrivano su questo telefono tramite il distributore, cifrate in modo che non possa leggerle.', + 'settings.push_poll': 'Attivate. Questo telefono controlla le nuove notifiche circa ogni quindici minuti. Per riceverle subito, puoi installare un’app distributore UnifiedPush come ntfy (facoltativo).', + 'settings.push_global_off': 'Non viene inviato nulla finché tutte le notifiche sono disattivate.', + 'push.channel_chat': 'Messaggi', + 'push.channel_other': 'Inviti e account', 'settings.defaults': 'Valori predefiniti', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index 23358c2..ad4ba25 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -432,6 +432,15 @@ export default { 'settings.email_verified': 'メールアドレスが正常に変更されました。', 'settings.notif_global_disable': 'Disable all notifications', 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.', + 'settings.push_label': 'この端末の通知', + 'settings.push_off': 'オフ。オンにすると、MeshBay を閉じていても新着メッセージや招待が通知されます。', + 'settings.push_pending': 'ディストリビューターの応答を待っています…', + 'settings.push_blocked': 'Android が MeshBay の通知をブロックしています。システム設定で許可してください。', + 'settings.push_ready': 'オン。新しい通知はディストリビューター経由でこの端末に届きます。暗号化されているため、ディストリビューターは内容を読めません。', + 'settings.push_poll': 'オン。この端末は約15分ごとに新しい通知を確認します。すぐに受け取りたい場合は、ntfy などの UnifiedPush ディストリビューターアプリをインストールできます(任意)。', + 'settings.push_global_off': 'すべての通知がオフの間は何も送信されません。', + 'push.channel_chat': 'メッセージ', + 'push.channel_other': '招待とアカウント', 'settings.defaults': 'デフォルト', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index 5e53084..78bada9 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -436,6 +436,15 @@ export default { 'settings.email_verified': 'E-mailadres succesvol gewijzigd.', 'settings.notif_global_disable': 'Alle meldingen uitschakelen', 'settings.notif_global_hint': 'When enabled, no notifications are created for any group.', + 'settings.push_label': 'Meldingen op deze telefoon', + 'settings.push_off': 'Uit. Zet aan om over nieuwe berichten en uitnodigingen te horen terwijl MeshBay gesloten is.', + 'settings.push_pending': 'Wachten op het antwoord van de distributor…', + 'settings.push_blocked': 'Android blokkeert de meldingen van MeshBay: sta ze toe in de systeeminstellingen.', + 'settings.push_ready': 'Aan. Nieuwe meldingen bereiken deze telefoon via de distributor, versleuteld zodat die ze niet kan lezen.', + 'settings.push_poll': 'Aan. Deze telefoon kijkt ongeveer elk kwartier of er nieuwe meldingen zijn. Wil je ze meteen ontvangen, installeer dan een UnifiedPush-distributor-app zoals ntfy (optioneel).', + 'settings.push_global_off': 'Er wordt niets verzonden zolang alle meldingen uit staan.', + 'push.channel_chat': 'Berichten', + 'push.channel_other': 'Uitnodigingen en account', 'settings.defaults': 'Standaardwaarden', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index 5d6d852..fd26974 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -447,6 +447,15 @@ export default { 'settings.email_verified': 'Adres e-mail został pomyślnie zmieniony.', 'settings.notif_global_disable': 'Wyłącz wszystkie powiadomienia', 'settings.notif_global_hint': 'Po włączeniu nie będą tworzone żadne powiadomienia dla żadnej grupy.', + 'settings.push_label': 'Powiadomienia na tym telefonie', + 'settings.push_off': 'Wyłączone. Włącz, aby dowiadywać się o nowych wiadomościach i zaproszeniach, gdy MeshBay jest zamknięty.', + 'settings.push_pending': 'Oczekiwanie na odpowiedź dystrybutora…', + 'settings.push_blocked': 'Android blokuje powiadomienia MeshBay: zezwól na nie w ustawieniach systemu.', + 'settings.push_ready': 'Włączone. Nowe powiadomienia docierają na ten telefon przez dystrybutora, zaszyfrowane tak, że nie może ich odczytać.', + 'settings.push_poll': 'Włączone. Ten telefon sprawdza nowe powiadomienia mniej więcej co piętnaście minut. Aby otrzymywać je od razu, możesz zainstalować aplikację dystrybutora UnifiedPush, np. ntfy (opcjonalnie).', + 'settings.push_global_off': 'Nic nie jest wysyłane, dopóki wszystkie powiadomienia są wyłączone.', + 'push.channel_chat': 'Wiadomości', + 'push.channel_other': 'Zaproszenia i konto', 'settings.defaults': 'Wartości domyślne', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 6fe50f6..e91ab10 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -436,6 +436,15 @@ export default { 'settings.email_verified': 'Endereço de e-mail alterado com sucesso.', 'settings.notif_global_disable': 'Desativar todas as notificações', 'settings.notif_global_hint': 'Quando ativado, nenhuma notificação é criada para nenhum grupo.', + 'settings.push_label': 'Notificações neste telefone', + 'settings.push_off': 'Desativadas. Ative para saber de novas mensagens e convites com o MeshBay fechado.', + 'settings.push_pending': 'Aguardando a resposta do distribuidor…', + 'settings.push_blocked': 'O Android está bloqueando as notificações do MeshBay: permita-as nas configurações do sistema.', + 'settings.push_ready': 'Ativadas. Novas notificações chegam a este telefone pelo distribuidor, criptografadas para que ele não possa lê-las.', + 'settings.push_poll': 'Ativadas. Este telefone verifica novas notificações a cada quinze minutos, aproximadamente. Para recebê-las na hora, você pode instalar um app distribuidor UnifiedPush como o ntfy (opcional).', + 'settings.push_global_off': 'Nada é enviado enquanto todas as notificações estiverem desativadas.', + 'push.channel_chat': 'Mensagens', + 'push.channel_other': 'Convites e conta', 'settings.defaults': 'Padrões', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index b3886ff..c030523 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -428,6 +428,15 @@ export default { 'settings.email_verified': '邮箱地址修改成功。', 'settings.notif_global_disable': '关闭所有通知', 'settings.notif_global_hint': '启用后,所有群组都不会创建通知。', + 'settings.push_label': '此手机上的通知', + 'settings.push_off': '已关闭。开启后,即使 MeshBay 已关闭,也会收到新消息和邀请的通知。', + 'settings.push_pending': '正在等待分发应用的响应…', + 'settings.push_blocked': 'Android 正在阻止 MeshBay 的通知:请在系统设置中允许。', + 'settings.push_ready': '已开启。新通知通过分发应用送达此手机,并经过加密,分发应用无法读取。', + 'settings.push_poll': '已开启。此手机大约每十五分钟检查一次新通知。如需即时收到,可以安装 UnifiedPush 分发应用,例如 ntfy(可选)。', + 'settings.push_global_off': '所有通知关闭期间不会发送任何内容。', + 'push.channel_chat': '消息', + 'push.channel_other': '邀请与账户', 'settings.defaults': '默认值', 'settings.default_tab': 'Default tab', 'settings.default_tab_hint': 'Which tab opens first when you enter a group.', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js index e3b3d2d..c70b03a 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js @@ -214,6 +214,22 @@ export const playback = { }, }; +/** + * Notifications on this device while the application is closed — fetched, or + * pushed through a UnifiedPush distributor when the phone has one. Only the + * Android application does this; `available` is false in a browser and on a + * desktop, and nothing here is then called. See push.js. + */ +export const push = { + available: Boolean(bridge && bridge.push), + status() { return bridge.push.status(); }, + enable() { return bridge.push.enable(); }, + disable() { return bridge.push.disable(); }, + remember(subscription, account, secret, since) { + return bridge.push.remember(subscription, account, secret, since); + }, +}; + /** Pick a directory to add as a group root. Returns { path, name } or null. */ export const rootPicker = { available: Boolean(bridge && bridge.rootPicker), diff --git a/packages/meshbay-hub/src/meshbay_hub/static/push.js b/packages/meshbay-hub/src/meshbay_hub/static/push.js new file mode 100644 index 0000000..a075aa4 --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/push.js @@ -0,0 +1,68 @@ +/** + * Notifications on this phone: the page's half. + * + * Nothing to install: the shell fetches what is new on its own every quarter + * of an hour, and when the phone already has a UnifiedPush distributor it is + * used too, so they arrive at once. The page gives the hub whatever the shell + * ends up with, because the page holds the session. Whether a notification is + * wanted — every one turned off, or a group muted — is decided on the hub, + * which then creates nothing (docs/MESHBAY_DESIGN.md §11.3). + */ +import * as platform from './platform.js'; +import { hubFetch } from './hub-client.js'; + +// How long turning it on waits for a distributor before registering without +// one; when the endpoint comes later, the next sync hands it over. +const WAIT_MS = 10_000; +const POLL_MS = 500; + +/** + * Make the hub's row match what this phone has: at every start and sign-in, + * and after turning it on. A distributor may hand out a new endpoint, or go + * away, at any time, page running or not; this is where the hub hears it. + */ +export async function syncPush(user) { + if (!platform.push.available || !user) return null; + const s = await platform.push.status(); + if (!s.enabled) return s; + const mine = s.subscription && s.account === user.userId; + if (mine && s.registered === s.endpoint) return s; + const body = s.endpoint + ? { endpoint: s.endpoint, p256dh: s.p256dh, auth: s.auth } + : {}; + if (mine) body.id = s.subscription; + const r = await hubFetch('/v1/push/subscriptions', { + method: 'POST', token: user.token, body, + }); + return platform.push.remember(r.id, user.userId, r.poll_secret, r.now); +} + +/** Turn it on: a moment for a distributor to answer, then register either way. */ +export async function enablePush(user, onProgress) { + let s = await platform.push.enable(); + const until = Date.now() + WAIT_MS; + while (s.state === 'pending' && Date.now() < until) { + if (onProgress) onProgress(s); + await new Promise((resolve) => setTimeout(resolve, POLL_MS)); + s = await platform.push.status(); + } + return syncPush(user).then((synced) => synced || s); +} + +/** + * Turn it off: the hub forgets this phone first, then the phone stops. The hub + * half is the one that can fail; the phone half happens regardless, so a + * refused request never leaves it on here. + */ +export async function disablePush(user) { + if (!platform.push.available) return null; + const s = await platform.push.status(); + if (s.subscription && user && s.account === user.userId) { + try { + await hubFetch(`/v1/push/subscriptions/${s.subscription}`, { + method: 'DELETE', token: user.token, + }); + } catch (e) { /* already gone, or unreachable: the phone half still happens */ } + } + return platform.push.disable(); +} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/settings-page.js b/packages/meshbay-hub/src/meshbay_hub/static/settings-page.js index 8755556..985b219 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/settings-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/settings-page.js @@ -5,6 +5,7 @@ import { t, getLocale, setLocale, LOCALES } from './i18n.js'; import * as downloads from './downloads.js'; import * as platform from './platform.js'; import { hubFetch } from './hub-client.js'; +import { syncPush, enablePush, disablePush } from './push.js'; import { APPS } from './apps.js'; import { PAGE_SIZE_PREF, PAGE_SIZE_DEFAULT, PAGE_SIZE_STEP, PAGE_SIZE_MAX, pageSizeFrom, @@ -81,6 +82,35 @@ export function SettingsPage({ user, theme, onThemeChange, groups, onPrefsChange } }, [globalMute, user.token, onPrefsChange]); + // Notifications on this phone (Android only: `platform.push.available`). + const [push, setPush] = useState(null); + const [pushBusy, setPushBusy] = useState(false); + useEffect(() => { + if (!platform.push.available) return; + syncPush(user).catch(() => platform.push.status()).then(setPush).catch(() => {}); + }, [user]); + + const togglePush = useCallback(async () => { + if (pushBusy) return; + setPushBusy(true); + try { + setPush(push && push.enabled ? await disablePush(user) : await enablePush(user, setPush)); + } catch (err) { + setPush(await platform.push.status().catch(() => null)); + } finally { + setPushBusy(false); + } + }, [push, pushBusy, user]); + + const pushHint = () => { + if (!push) return null; + if (globalMute) return t('settings.push_global_off'); + if (!push.enabled) return t('settings.push_off'); + if (!push.permitted) return t('settings.push_blocked'); + if (push.state === 'pending') return t('settings.push_pending'); + return push.endpoint ? t('settings.push_ready') : t('settings.push_poll'); + }; + const toggleMute = useCallback(async (gid) => { const next = !muted[gid]; setMuted(prev => ({ ...prev, [gid]: next })); @@ -240,6 +270,17 @@ export function SettingsPage({ user, theme, onThemeChange, groups, onPrefsChange <span class="toggle-switch-track"><span class="toggle-switch-thumb"></span></span> </label> </div> + ${push && html` + <div class="settings-row"> + <span class="settings-label">${t('settings.push_label')}</span> + <label class="toggle-switch"> + <input type="checkbox" checked=${!!push.enabled} disabled=${pushBusy} + onChange=${togglePush} /> + <span class="toggle-switch-track"><span class="toggle-switch-thumb"></span></span> + </label> + </div> + <p class="settings-hint">${pushHint()}</p> + `} ${!globalMute && html` <p class="settings-hint" style="margin-bottom:8px">${t('settings.notif_global_hint')}</p> ${groups.map(g => html` |