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
Show file tree
Hide file tree
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
Expand Up @@ -25,11 +25,11 @@ const RefundButton = ({
setRefundTxId?: Setter<string>;
}) => {
const {
notify,
getSwap,
setSwapStorage,
setRefundAddress,
refundAddress,
notify,
t,
} = useGlobalContext();
const { setSwap } = usePayContext();
Expand Down
7 changes: 7 additions & 0 deletions src/components/SettingsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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" />
Expand Down
21 changes: 16 additions & 5 deletions src/components/SwapChecker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
}
}
};
Expand Down
31 changes: 29 additions & 2 deletions src/context/Global.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -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()));
Expand Down Expand Up @@ -345,6 +370,8 @@ const GlobalProvider = (props: { children: any }) => {
setSettingsMenu,
audioNotification,
setAudioNotification,
browserNotification,
setBrowserNotification,
// functions
t,
notify,
Expand Down
48 changes: 34 additions & 14 deletions src/i18n/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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: "中文",
Expand Down Expand Up @@ -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: "通知权限被拒绝",
},
};

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

#notification {
background: black;
color: white;
}
.toggle {
cursor: pointer;
flex-grow: 0;
Expand Down Expand Up @@ -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;
Expand Down
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;
Expand All @@ -8,7 +12,7 @@
color: #fff;
text-align: center;
position: fixed;
z-index: 1;
z-index: 9999;
left: 0;
right: 0;
bottom: 30px;
Expand Down
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
Expand Up @@ -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>;