feat: polish

This commit is contained in:
reya 2024-01-18 15:09:16 +07:00
parent 0e9418949b
commit ed6423e4aa
6 changed files with 176 additions and 141 deletions

View File

@ -1,7 +1,16 @@
import { activityUnreadAtom } from "@lume/utils";
import { useSetAtom } from "jotai";
import { useEffect } from "react";
import { Outlet } from "react-router-dom"; import { Outlet } from "react-router-dom";
import { ActivityList } from "./components/list"; import { ActivityList } from "./components/list";
export function ActivityScreen() { export function ActivityScreen() {
const setUnreadActivity = useSetAtom(activityUnreadAtom);
useEffect(() => {
setUnreadActivity(0);
}, []);
return ( return (
<div className="flex h-full w-full rounded-xl shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:shadow-none dark:ring-1 dark:ring-white/10"> <div className="flex h-full w-full rounded-xl shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:shadow-none dark:ring-1 dark:ring-white/10">
<div className="h-full flex flex-col w-96 shrink-0 rounded-l-xl bg-white/50 backdrop-blur-xl dark:bg-black/50"> <div className="h-full flex flex-col w-96 shrink-0 rounded-l-xl bg-white/50 backdrop-blur-xl dark:bg-black/50">

View File

@ -1,4 +1,3 @@
import { webln } from "@getalby/sdk";
import { useArk } from "@lume/ark"; import { useArk } from "@lume/ark";
import { useStorage } from "@lume/storage"; import { useStorage } from "@lume/storage";
import * as Switch from "@radix-ui/react-switch"; import * as Switch from "@radix-ui/react-switch";
@ -28,7 +27,10 @@ export function NWCScreen() {
const params = new URLSearchParams(uriObj.search); const params = new URLSearchParams(uriObj.search);
if (params.has("relay") && params.has("secret")) { if (params.has("relay") && params.has("secret")) {
await storage.createPrivkey("Nostr Wallet Connect", walletConnectURL); await storage.createPrivkey(
`${ark.account.pubkey}.nwc`,
walletConnectURL,
);
storage.nwc = walletConnectURL; storage.nwc = walletConnectURL;
@ -52,19 +54,11 @@ export function NWCScreen() {
}; };
const remove = async () => { const remove = async () => {
await storage.removePrivkey(`${ark.account.pubkey}-nwc`); await storage.removePrivkey(`${ark.account.pubkey}.nwc`);
setWalletConnectURL(null);
};
const loadBalance = async () => { setWalletConnectURL("");
const nwc = new webln.NostrWebLNProvider({ setSettings((state) => ({ ...state, nwc: false }));
nostrWalletConnectUrl: walletConnectURL, storage.nwc = null;
});
await nwc.enable();
const balanceResponse = await nwc.getBalance();
nwc.close();
}; };
useEffect(() => { useEffect(() => {
@ -78,37 +72,34 @@ export function NWCScreen() {
<div className="mx-auto w-full max-w-lg"> <div className="mx-auto w-full max-w-lg">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex w-full items-center justify-between"> <div className="flex w-full items-center justify-between">
<div className="flex w-full items-center gap-8"> <div className="flex w-full items-start gap-8">
<div className="w-36 shrink-0 text-end text-sm font-semibold"> <div className="w-36 shrink-0 text-end text-sm font-semibold">
Connection String Connection String
</div> </div>
<div className="relative w-full"> <div className="flex flex-col items-end gap-2 w-full">
<input <textarea
type="password"
spellCheck={false} spellCheck={false}
value={walletConnectURL} value={walletConnectURL}
onChange={(e) => setWalletConnectURL(e.target.value)} onChange={(e) => setWalletConnectURL(e.target.value)}
className="w-full border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-9 rounded-lg ring-0 placeholder:text-neutral-600 bg-neutral-100 dark:bg-neutral-900" className="w-full h-24 resize-none border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none rounded-lg ring-0 placeholder:text-neutral-600 bg-neutral-100 dark:bg-neutral-900"
/> />
<div className="h-9 absolute right-0 top-0 inline-flex items-center justify-center"> {!settings.nwc ? (
{!settings.nwc ? ( <button
<button type="button"
type="button" onClick={saveNWC}
onClick={saveNWC} className="h-8 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
className="mr-1 h-7 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700" >
> Save
Save </button>
</button> ) : (
) : ( <button
<button type="button"
type="button" onClick={remove}
onClick={remove} className="h-8 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
className="mr-1 h-7 w-16 text-sm font-medium shrink-0 inline-flex items-center justify-center rounded-md bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700" >
> Remove
Remove </button>
</button> )}
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,15 +5,14 @@ import { useStorage } from "@lume/storage";
import { cn, compactNumber, displayNpub } from "@lume/utils"; import { cn, compactNumber, displayNpub } from "@lume/utils";
import * as Dialog from "@radix-ui/react-dialog"; import * as Dialog from "@radix-ui/react-dialog";
import * as Tooltip from "@radix-ui/react-tooltip"; import * as Tooltip from "@radix-ui/react-tooltip";
import { QRCodeSVG } from "qrcode.react";
import { useState } from "react"; import { useState } from "react";
import CurrencyInput from "react-currency-input-field"; import CurrencyInput from "react-currency-input-field";
import { toast } from "sonner"; import { toast } from "sonner";
import { useArk } from "../../../hooks/useArk";
import { useProfile } from "../../../hooks/useProfile"; import { useProfile } from "../../../hooks/useProfile";
import { useNoteContext } from "../provider"; import { useNoteContext } from "../provider";
export function NoteZap() { export function NoteZap() {
const ark = useArk();
const storage = useStorage(); const storage = useStorage();
const event = useNoteContext(); const event = useNoteContext();
@ -22,11 +21,12 @@ export function NoteZap() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isCompleted, setIsCompleted] = useState(false); const [isCompleted, setIsCompleted] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [invoice, setInvoice] = useState<string>(null);
const { user } = useProfile(event.pubkey); const { user } = useProfile(event.pubkey);
const createZapRequest = async (instant?: boolean) => { const createZapRequest = async (instant?: boolean) => {
if (!storage.nwc) return; if (instant && !storage.nwc) return;
let nwc: webln.NostrWebLNProvider = undefined; let nwc: webln.NostrWebLNProvider = undefined;
@ -37,6 +37,8 @@ export function NoteZap() {
const zapAmount = parseInt(amount) * 1000; const zapAmount = parseInt(amount) * 1000;
const res = await event.zap(zapAmount, zapMessage); const res = await event.zap(zapAmount, zapMessage);
if (!storage.nwc) return setInvoice(res);
// user connect nwc // user connect nwc
nwc = new webln.NostrWebLNProvider({ nwc = new webln.NostrWebLNProvider({
nostrWalletConnectUrl: storage.nwc, nostrWalletConnectUrl: storage.nwc,
@ -144,101 +146,105 @@ export function NoteZap() {
<CancelIcon className="w-4 h-4" /> <CancelIcon className="w-4 h-4" />
</Dialog.Close> </Dialog.Close>
</div> </div>
<div className="px-5 pb-5 overflow-x-hidden overflow-y-auto"> {!invoice ? (
<div className="relative flex flex-col h-40"> <div className="px-5 pb-5 overflow-x-hidden overflow-y-auto">
<div className="inline-flex items-center justify-center flex-1 h-full gap-1"> <div className="relative flex flex-col h-40">
<CurrencyInput <div className="inline-flex items-center justify-center flex-1 h-full gap-1">
placeholder="0" <CurrencyInput
defaultValue={"21"} placeholder="0"
value={amount} defaultValue={"21"}
decimalsLimit={2} value={amount}
min={0} // 0 sats decimalsLimit={2}
max={10000} // 1M sats min={0} // 0 sats
maxLength={10000} // 1M sats max={10000} // 1M sats
onValueChange={(value) => setAmount(value)} maxLength={10000} // 1M sats
className="flex-1 w-full text-4xl font-semibold text-right bg-transparent border-none placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400" onValueChange={(value) => setAmount(value)}
className="flex-1 w-full text-4xl font-semibold text-right bg-transparent border-none placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
/>
<span className="flex-1 w-full text-4xl font-semibold text-left text-neutral-500 dark:text-neutral-400">
sats
</span>
</div>
<div className="inline-flex items-center justify-center gap-2">
<button
type="button"
onClick={() => setAmount("69")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
69 sats
</button>
<button
type="button"
onClick={() => setAmount("100")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
100 sats
</button>
<button
type="button"
onClick={() => setAmount("200")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
200 sats
</button>
<button
type="button"
onClick={() => setAmount("500")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
500 sats
</button>
<button
type="button"
onClick={() => setAmount("1000")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
1K sats
</button>
</div>
</div>
<div className="flex flex-col w-full gap-2 mt-4">
<input
name="zapMessage"
value={zapMessage}
onChange={(e) => setZapMessage(e.target.value)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="Enter message (optional)"
className="w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-400"
/> />
<span className="flex-1 w-full text-4xl font-semibold text-left text-neutral-500 dark:text-neutral-400"> <div className="flex flex-col gap-2">
sats <button
type="button"
onClick={() => createZapRequest()}
className="inline-flex items-center justify-center w-full px-4 font-medium text-white bg-blue-500 rounded-lg h-11 hover:bg-blue-600"
>
{isCompleted
? "Zapped"
: isLoading
? "Processing..."
: "Zap"}
</button>
</div>
</div>
</div>
) : (
<div className="px-5 pb-5 flex flex-col items-center justify-center gap-4">
<div className="rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
<QRCodeSVG value={invoice} size={256} />
</div>
<div className="flex flex-col items-center gap-1">
<h3 className="text-lg font-medium">Scan to zap</h3>
<span className="text-center text-sm text-neutral-600 dark:text-neutral-400">
You must use Bitcoin wallet which support Lightning
<br />
such as: Blue Wallet, Bitkit, Phoenix,...
</span> </span>
</div> </div>
<div className="inline-flex items-center justify-center gap-2">
<button
type="button"
onClick={() => setAmount("69")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
69 sats
</button>
<button
type="button"
onClick={() => setAmount("100")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
100 sats
</button>
<button
type="button"
onClick={() => setAmount("200")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
200 sats
</button>
<button
type="button"
onClick={() => setAmount("500")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
500 sats
</button>
<button
type="button"
onClick={() => setAmount("1000")}
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
1K sats
</button>
</div>
</div> </div>
<div className="flex flex-col w-full gap-2 mt-4"> )}
<input
name="zapMessage"
value={zapMessage}
onChange={(e) => setZapMessage(e.target.value)}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="Enter message (optional)"
className="w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 py-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-400"
/>
<div className="flex flex-col gap-2">
<button
type="button"
onClick={() => createZapRequest()}
className="inline-flex items-center justify-center w-full px-4 font-medium text-white bg-blue-500 rounded-lg h-11 hover:bg-blue-600"
>
{isCompleted ? (
<p className="leading-tight">Successfully zapped</p>
) : isLoading ? (
<span className="flex flex-col">
<p className="leading-tight">Waiting for approval</p>
<p className="text-xs leading-tight text-neutral-100">
Go to your wallet and approve payment request
</p>
</span>
) : (
<span className="flex flex-col">
<p className="leading-tight">Send zap</p>
<p className="text-xs leading-tight text-neutral-100">
You&apos;re using nostr wallet connect
</p>
</span>
)}
</button>
</div>
</div>
</div>
</div> </div>
</Dialog.Content> </Dialog.Content>
</Dialog.Portal> </Dialog.Portal>

View File

@ -1,7 +1,12 @@
import { LoaderIcon } from "@lume/icons"; import { LoaderIcon } from "@lume/icons";
import { NDKCacheAdapterTauri } from "@lume/ndk-cache-tauri"; import { NDKCacheAdapterTauri } from "@lume/ndk-cache-tauri";
import { useStorage } from "@lume/storage"; import { useStorage } from "@lume/storage";
import { FETCH_LIMIT, QUOTES, sendNativeNotification } from "@lume/utils"; import {
FETCH_LIMIT,
QUOTES,
activityUnreadAtom,
sendNativeNotification,
} from "@lume/utils";
import NDK, { import NDK, {
NDKEvent, NDKEvent,
NDKKind, NDKKind,
@ -14,6 +19,7 @@ import NDK, {
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { message } from "@tauri-apps/plugin-dialog"; import { message } from "@tauri-apps/plugin-dialog";
import { fetch } from "@tauri-apps/plugin-http"; import { fetch } from "@tauri-apps/plugin-http";
import { useSetAtom } from "jotai";
import Linkify from "linkify-react"; import Linkify from "linkify-react";
import { normalizeRelayUrlSet } from "nostr-fetch"; import { normalizeRelayUrlSet } from "nostr-fetch";
import { PropsWithChildren, useEffect, useState } from "react"; import { PropsWithChildren, useEffect, useState } from "react";
@ -23,6 +29,7 @@ import { LumeContext } from "./context";
export const LumeProvider = ({ children }: PropsWithChildren<object>) => { export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
const storage = useStorage(); const storage = useStorage();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const setUnreadActivity = useSetAtom(activityUnreadAtom);
const [ark, setArk] = useState<Ark>(undefined); const [ark, setArk] = useState<Ark>(undefined);
const [ndk, setNDK] = useState<NDK>(undefined); const [ndk, setNDK] = useState<NDK>(undefined);
@ -66,6 +73,11 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
const userPrivkey = await storage.loadPrivkey(storage.currentUser.pubkey); const userPrivkey = await storage.loadPrivkey(storage.currentUser.pubkey);
if (!userPrivkey) return null; if (!userPrivkey) return null;
// load nwc
storage.nwc = await storage.loadPrivkey(
`${storage.currentUser.pubkey}.nwc`,
);
return new NDKPrivateKeySigner(userPrivkey); return new NDKPrivateKeySigner(userPrivkey);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@ -99,7 +111,7 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
enableOutboxModel: !storage.settings.lowPower, enableOutboxModel: !storage.settings.lowPower,
autoConnectUserRelays: !storage.settings.lowPower, autoConnectUserRelays: !storage.settings.lowPower,
autoFetchUserMutelist: !storage.settings.lowPower, autoFetchUserMutelist: !storage.settings.lowPower,
// clientName: "Lume", clientName: "Lume",
// clientNip89: '', // clientNip89: '',
}); });
@ -120,7 +132,7 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
ndk.relayAuthDefaultPolicy = async (relay: NDKRelay, challenge: string) => { ndk.relayAuthDefaultPolicy = async (relay: NDKRelay, challenge: string) => {
const signIn = NDKRelayAuthPolicies.signIn({ ndk }); const signIn = NDKRelayAuthPolicies.signIn({ ndk });
const event = await signIn(relay, challenge).catch((e) => const event = await signIn(relay, challenge).catch((e) =>
console.error(e), console.log("auth failed", e),
); );
if (event) { if (event) {
await sendNativeNotification( await sendNativeNotification(
@ -147,7 +159,7 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
await ark.getUserContacts(); await ark.getUserContacts();
// subscribe for new activity // subscribe for new activity
const notifySub = ndk.subscribe( const activitySub = ndk.subscribe(
{ {
kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Zap], kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Zap],
since: Math.floor(Date.now() / 1000), since: Math.floor(Date.now() / 1000),
@ -156,25 +168,26 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
{ closeOnEose: false, groupable: false }, { closeOnEose: false, groupable: false },
); );
notifySub.addListener("event", async (event: NDKEvent) => { activitySub.addListener("event", async (event: NDKEvent) => {
setUnreadActivity((state) => state + 1);
const profile = await ark.getUserProfile(event.pubkey); const profile = await ark.getUserProfile(event.pubkey);
switch (event.kind) { switch (event.kind) {
case NDKKind.Text: case NDKKind.Text:
return await sendNativeNotification( return await sendNativeNotification(
`${ `${
profile.displayName || profile.name || "anon" profile.displayName || profile.name || "Anon"
} has replied to your note`, } has replied to your note`,
); );
case NDKKind.Repost: case NDKKind.Repost:
return await sendNativeNotification( return await sendNativeNotification(
`${ `${
profile.displayName || profile.name || "anon" profile.displayName || profile.name || "Anon"
} has reposted to your note`, } has reposted to your note`,
); );
case NDKKind.Zap: case NDKKind.Zap:
return await sendNativeNotification( return await sendNativeNotification(
`${ `${
profile.displayName || profile.name || "anon" profile.displayName || profile.name || "Anon"
} has zapped to your note`, } has zapped to your note`,
); );
default: default:

View File

@ -15,6 +15,7 @@ import { useAtom } from "jotai";
import { useHotkeys } from "react-hotkeys-hook"; import { useHotkeys } from "react-hotkeys-hook";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { ActiveAccount } from "./account/active"; import { ActiveAccount } from "./account/active";
import { UnreadActivity } from "./unread";
export function Navigation() { export function Navigation() {
const [isEditorOpen, setIsEditorOpen] = useAtom(editorAtom); const [isEditorOpen, setIsEditorOpen] = useAtom(editorAtom);
@ -74,7 +75,7 @@ export function Navigation() {
{({ isActive }) => ( {({ isActive }) => (
<div <div
className={cn( className={cn(
"inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl", "relative inline-flex aspect-square h-auto w-full items-center justify-center rounded-xl",
isActive isActive
? "bg-black/10 text-black dark:bg-white/10 dark:text-white" ? "bg-black/10 text-black dark:bg-white/10 dark:text-white"
: "text-black/50 dark:text-neutral-400", : "text-black/50 dark:text-neutral-400",
@ -85,6 +86,7 @@ export function Navigation() {
) : ( ) : (
<BellIcon className="size-6" /> <BellIcon className="size-6" />
)} )}
<UnreadActivity />
</div> </div>
)} )}
</NavLink> </NavLink>

View File

@ -0,0 +1,14 @@
import { activityUnreadAtom, compactNumber } from "@lume/utils";
import { useAtomValue } from "jotai";
export function UnreadActivity() {
const total = useAtomValue(activityUnreadAtom);
if (total <= 0) return null;
return (
<div className="absolute -right-0.5 -top-0.5 inline-flex size-5 items-center justify-center rounded-full bg-teal-500 text-[9px] font-medium text-white">
{compactNumber.format(total)}
</div>
);
}