diff --git a/packages/app/src/Pages/Layout.tsx b/packages/app/src/Pages/Layout.tsx
index bfe5a0a1..415299a8 100644
--- a/packages/app/src/Pages/Layout.tsx
+++ b/packages/app/src/Pages/Layout.tsx
@@ -25,6 +25,7 @@ import Avatar from "Element/Avatar";
import { useUserProfile } from "Hooks/useUserProfile";
import { profileLink } from "Util";
import { getCurrentSubscription } from "Subscription";
+import Toaster from "Toaster";
export default function Layout() {
const location = useLocation();
@@ -170,6 +171,7 @@ export default function Layout() {
>
)}
{window.localStorage.getItem("debug") && }
+
);
}
diff --git a/packages/app/src/Pages/ZapPool.tsx b/packages/app/src/Pages/ZapPool.tsx
index 7bc46e95..c2fe1280 100644
--- a/packages/app/src/Pages/ZapPool.tsx
+++ b/packages/app/src/Pages/ZapPool.tsx
@@ -74,7 +74,7 @@ export default function ZapPoolPage() {
const sumPending = zapPool.reduce((acc, v) => acc + v.sum, 0);
return (
-
+
diff --git a/packages/app/src/Toaster.css b/packages/app/src/Toaster.css
new file mode 100644
index 00000000..a5ada87d
--- /dev/null
+++ b/packages/app/src/Toaster.css
@@ -0,0 +1,12 @@
+.toaster {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ display: flex;
+ flex-direction: column-reverse;
+ z-index: 9999;
+}
+
+.toaster > .card {
+ border: 1px solid var(--gray);
+}
diff --git a/packages/app/src/Toaster.tsx b/packages/app/src/Toaster.tsx
new file mode 100644
index 00000000..4b48c887
--- /dev/null
+++ b/packages/app/src/Toaster.tsx
@@ -0,0 +1,53 @@
+import ExternalStore from "ExternalStore";
+import Icon from "Icons/Icon";
+import { ReactNode, useSyncExternalStore } from "react";
+import { unixNow } from "Util";
+
+import "./Toaster.css";
+
+interface ToastNotification {
+ element: ReactNode;
+ expire?: number;
+ icon?: string;
+}
+
+class ToasterSlots extends ExternalStore
> {
+ #stack: Array = [];
+ #cleanup = setInterval(() => this.#eatToast(), 1000);
+
+ push(n: ToastNotification) {
+ n.expire ??= unixNow() + 3;
+ this.#stack.push(n);
+ this.notifyChange();
+ }
+
+ takeSnapshot(): ToastNotification[] {
+ return [...this.#stack];
+ }
+
+ #eatToast() {
+ const now = unixNow();
+ this.#stack = this.#stack.filter(a => (a.expire ?? 0) > now);
+ this.notifyChange();
+ }
+}
+
+export const Toastore = new ToasterSlots();
+
+export default function Toaster() {
+ const toast = useSyncExternalStore(
+ c => Toastore.hook(c),
+ () => Toastore.snapshot()
+ );
+
+ return (
+
+ {toast.map(a => (
+
+
+ {a.element}
+
+ ))}
+
+ );
+}
diff --git a/packages/app/src/ZapPoolController.ts b/packages/app/src/ZapPoolController.ts
index d02dc997..8ee240f2 100644
--- a/packages/app/src/ZapPoolController.ts
+++ b/packages/app/src/ZapPoolController.ts
@@ -1,6 +1,9 @@
import { UserCache } from "Cache";
+import { getDisplayName } from "Element/ProfileImage";
import ExternalStore from "ExternalStore";
import { LNURL } from "LNURL";
+import { Toastore } from "Toaster";
+import { unixNow } from "Util";
import { LNWallet, WalletInvoiceState } from "Wallet";
export enum ZapPoolRecipientType {
@@ -46,6 +49,14 @@ class ZapPool extends ExternalStore> {
const result = await wallet.payInvoice(invoice.pr);
if (result.state === WalletInvoiceState.Paid) {
x.sum -= amtSend;
+ Toastore.push({
+ element: `Sent ${amtSend.toLocaleString()} sats to ${getDisplayName(
+ profile,
+ x.pubkey
+ )} from your zap pool`,
+ expire: unixNow() + 10,
+ icon: "zap",
+ });
} else {
throw new Error("Payment failed");
}