Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add browser notification #528

Merged
merged 18 commits into from
May 23, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 19 additions & 20 deletions public/boltz-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions src/components/BrowserNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useGlobalContext } from "../context/Global";
import { registerNotifications } from "../utils/notification";

const BrowserNotification = () => {
const { browserNotification, setBrowserNotification, t, notify } =
useGlobalContext();

const toggle = (evt: MouseEvent) => {
// When disabled, we try to request permission and enable them
if (!browserNotification()) {
registerNotifications().then((state: boolean) => {
setBrowserNotification(state);
if (state === false) {
notify("error", t("browsernotification_error"));
}
});
evt.stopPropagation();
return;
}
// When enabled, we disable sending them
setBrowserNotification(false);
evt.stopPropagation();
};

return (
<>
<div
class="browser-notification toggle"
title={t("browsernotification_tooltip")}
onClick={toggle}>
<span class={browserNotification() ? "active" : ""}>
{t("on")}
</span>
<span class={!browserNotification() ? "active" : ""}>
{t("off")}
</span>
</div>
</>
);
};

export default BrowserNotification;
2 changes: 1 addition & 1 deletion src/components/RefundButton.tsx
Original file line number Diff line number Diff line change
@@ -25,11 +25,11 @@ const RefundButton = ({
setRefundTxId?: Setter<string>;
}) => {
const {
notify,
getSwap,
setSwapStorage,
setRefundAddress,
refundAddress,
notify,
t,
} = useGlobalContext();
const { setSwap } = usePayContext();
7 changes: 7 additions & 0 deletions src/components/SettingsMenu.tsx
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import { IoClose } from "solid-icons/io";
import { useGlobalContext } from "../context/Global";
import "../style/settings.scss";
import AudioNotificationSetting from "./AudioNotificationSetting";
import BrowserNotification from "./BrowserNotification";
import Denomination from "./Denomination";
import Logs from "./Logs";
import Separator from "./Separator";
@@ -41,6 +42,12 @@ const SettingsMenu = () => {
<div class="spacer"></div>
<AudioNotificationSetting />
</span>
<span class="setting">
<label>{t("browsernotification")}: </label>
<Tooltip label="browsernotification_tooltip" />
<div class="spacer"></div>
<BrowserNotification />
</span>
<span class="setting">
<label>{t("logs")}: </label>
<Tooltip label="logs_tooltip" />
21 changes: 16 additions & 5 deletions src/components/SwapChecker.tsx
Original file line number Diff line number Diff line change
@@ -211,7 +211,12 @@ export const SwapChecker = () => {
if (claimedSwap.id === swap().id) {
setSwap(claimedSwap);
}
notify("success", t("claim_success", { id: res.id }), true);
notify(
"success",
t("swap_completed", { id: res.id }),
true,
true,
);
} catch (e) {
const msg = t("claim_fail", { id: currentSwap.id });
log.warn(msg, e);
@@ -220,11 +225,17 @@ export const SwapChecker = () => {
} else if (data.status === swapStatusPending.TransactionClaimPending) {
try {
await createSubmarineSignature(currentSwap);
} catch (e) {
log.warn(
`creating cooperative signature for submarine swap claim failed`,
e,
notify(
"success",
t("swap_completed", { id: currentSwap.id }),
true,
true,
);
} catch (e) {
const msg =
"creating cooperative signature for submarine swap claim failed";
log.warn(msg, e);
notify("error", msg);
}
}
};
31 changes: 29 additions & 2 deletions src/context/Global.tsx
Original file line number Diff line number Diff line change
@@ -63,9 +63,16 @@ export type GlobalContextType = {
setSettingsMenu: Setter<boolean>;
audioNotification: Accessor<boolean>;
setAudioNotification: Setter<boolean>;
browserNotification: Accessor<boolean>;
setBrowserNotification: Setter<boolean>;
// functions
t: (key: string, values?: Record<string, any>) => string;
notify: (type: string, message: string, audio?: boolean) => void;
notify: (
type: string,
message: string,
browser?: boolean,
audio?: boolean,
) => void;
playNotificationSound: () => void;
fetchPairs: (asset?: string) => void;

@@ -171,10 +178,21 @@ const GlobalProvider = (props: { children: any }) => {
},
);

const notify = (type: string, message: string, audio: boolean = false) => {
const notify = (
type: string,
message: string,
browser: boolean = false,
audio: boolean = false,
) => {
setNotificationType(type);
setNotification(message);
if (audio && audioNotification()) playNotificationSound();
if (browser && browserNotification()) {
new Notification(t("notification_header"), {
body: message,
icon: "/boltz-icon.svg",
});
}
};

const playNotificationSound = () => {
@@ -294,6 +312,13 @@ const GlobalProvider = (props: { children: any }) => {
setEmbedded(true);
}

const [browserNotification, setBrowserNotification] = makePersisted(
createSignal<boolean>(false),
{
name: "browserNotification",
},
);

// i18n
let dictLocale: any;
createMemo(() => setI18n(i18nConfigured()));
@@ -345,6 +370,8 @@ const GlobalProvider = (props: { children: any }) => {
setSettingsMenu,
audioNotification,
setAudioNotification,
browserNotification,
setBrowserNotification,
// functions
t,
notify,
48 changes: 34 additions & 14 deletions src/i18n/i18n.ts
Original file line number Diff line number Diff line change
@@ -61,7 +61,8 @@ const dict = {
pay_timeout_blockheight: "Timeout block height",
pay_expected_amount: "Expected amount",
send_to: "Send {{ amount }} {{ denomination }} to",
pay_invoice_to: "Pay this invoice for {{ amount }} {{ denomination }}",
pay_invoice_to:
"Pay this invoice about {{ amount }} {{ denomination }}",
pay_address: "Address",
no_metamask: "MetaMask not installed",
connect_metamask: "Connect MetaMask",
@@ -182,15 +183,19 @@ const dict = {
denomination_tooltip: "Choose your preferred denomination: BTC or sats",
decimal_tooltip:
"Choose your preferred decimal separator: dot or comma",
claim_success: "Swap {{ id }} claimed successfully!",
swap_completed: "Swap {{ id }} completed successfully!",
claim_fail: "Failed to claim swap: {{ id }}",
logs: "Logs",
logs_tooltip: "Logs of the web app, useful for debugging.",
enable_audio_notifications: "Enable Audio Notifications",
logs_tooltip: "Logs of the web app, useful for debugging",
enable_audio_notifications: "Audio Notifications",
enable_audio_notifications_tooltip:
"Enable or disable audio notifications",
on: "on",
off: "off",
notification_header: "Boltz",
browsernotification: "Browser Notifications",
browsernotification_tooltip: "Enable or disable browser notifications",
browsernotification_error: "Notification permissions denied",
},
de: {
language: "Deutsch",
@@ -255,7 +260,8 @@ const dict = {
pay_timeout_blockheight: "Timeout Blockhöhe",
pay_expected_amount: "Erwarteter Betrag",
send_to: "Sende {{ amount }} {{ denomination }} an",
pay_invoice_to: "Zahle Rechnung über {{ amount }} {{ denomination }}",
pay_invoice_to:
"Zahle diese Rechnung über {{ amount }} {{ denomination }}",
pay_address: "Adresse",
no_metamask: "MetaMask ist nicht installiert",
connect_metamask: "MetaMask verbinden",
@@ -382,15 +388,20 @@ const dict = {
"Wähle deine bevorzugte Denomination: BTC oder sats",
decimal_tooltip:
"Wähle dein bevorzugtes Dezimaltrennzeichen: Punkt oder Komma",
claim_success: "Swap {{ id }} erfolgreich geclaimed!",
swap_completed: "Swap {{ id }} erfolgreich abgeschlossen!",
claim_fail: "Swap {{ id }} konnte nicht geclaimed werden!",
logs: "Logs",
logs_tooltip: "Logs der Web App, nützlich für Debugging.",
enable_audio_notifications: "Audio Benachrichtigungen aktivieren",
logs_tooltip: "Logs der Web App, nützlich für Debugging",
enable_audio_notifications: "Audio Benachrichtigungen",
enable_audio_notifications_tooltip:
"Aktiviere oder deaktiviere Audio-Benachrichtigungen",
on: "an",
off: "aus",
notification_header: "Boltz",
browsernotification: "Browser Benachrichtigungen",
browsernotification_tooltip:
"Aktiviere oder deaktiviere Browser Benachrichtigungen",
browsernotification_error: "Benachrichtigungsrechte verweigert",
},
es: {
language: "Español",
@@ -582,16 +593,21 @@ const dict = {
decimal_separator: "Separador decimal",
denomination_tooltip: "Elige tu denominación preferida: BTC o sats",
decimal_tooltip: "Elige tu separador decimal preferido: punto o coma",
claim_success: "¡Intercambio {{ id }} reclamado con éxito!",
swap_completed: "¡Intercambio {{ id }} completado con éxito!",
claim_fail: "¡Error en reclamar el intercambio {{ id }}!",
logs: "Logs",
logs_tooltip:
"Registros de la aplicación web como herramienta de depuración.",
enable_audio_notifications: "Activar notificaciones de audio",
"Registros de la aplicación web como herramienta de depuración",
enable_audio_notifications: "Notificaciones de Audio",
enable_audio_notifications_tooltip:
"Activar o desactivar notificaciones de audio",
on: "on",
off: "off",
notification_header: "Boltz",
browsernotification: "Notificaciones del navegador",
browsernotification_tooltip:
"Activar o desactivar notificaciones del navegador",
browsernotification_error: "Permisos de notificación denegados",
},
zh: {
language: "中文",
@@ -762,14 +778,18 @@ const dict = {
decimal_separator: "小数分隔符",
denomination_tooltip: "选择您的首选面额:BTC 或 sats",
decimal_tooltip: "选择您的首选小数分隔符:点或逗号",
claim_success: "交换{{ id }}成功索赔!",
swap_completed: "交换{{ id }} 已成功完成!",
claim_fail: "交换{{ id }}索赔失败!",
logs: "日志",
logs_tooltip: "网络应用程序的日志,用于调试",
enable_audio_notifications: "启用音频通知",
logs_tooltip: "网络应用程序的日志,用于调试",
enable_audio_notifications: "音频通知",
enable_audio_notifications_tooltip: "启用或禁用音频通知",
on: "开",
off: "关",
notification_header: "Boltz",
browsernotification: "浏览器通知",
browsernotification_tooltip: "启用或禁用浏览器通知",
browsernotification_error: "通知权限被拒绝",
},
};

6 changes: 1 addition & 5 deletions src/style/index.scss
Original file line number Diff line number Diff line change
@@ -344,10 +344,6 @@ textarea {
display: block;
}

#notification {
background: black;
color: white;
}
.toggle {
cursor: pointer;
flex-grow: 0;
@@ -508,7 +504,7 @@ textarea {
width: 100%;
left: 0;
top: 64px;
z-index: 9999;
z-index: 999;
display: none;
box-shadow: 0 0 12px black;
margin: 0;
6 changes: 5 additions & 1 deletion src/style/notification.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
@import "vars";

#notification {
background: black;
color: white;
}
#notification {
visibility: hidden;
max-width: 480px;
@@ -8,7 +12,7 @@
color: #fff;
text-align: center;
position: fixed;
z-index: 1;
z-index: 9999;
left: 0;
right: 0;
bottom: 30px;
10 changes: 10 additions & 0 deletions src/utils/notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import log from "loglevel";

export const registerNotifications = () => {
return new Promise<boolean>((resolve) => {
Notification.requestPermission().then((result) => {
log.info("Notification permission: ", result);
resolve(result === "granted");
});
});
};
5 changes: 5 additions & 0 deletions tests/setup.ts
Original file line number Diff line number Diff line change
@@ -3,3 +3,8 @@ import regtest from "../src/configs/regtest.json";

regtest.loglevel = "error";
setConfig(regtest);

globalThis.Notification = {
requestPermission: jest.fn().mockResolvedValue(true),
permission: "granted",
} as unknown as jest.Mocked<typeof Notification>;