mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
feat: move nwc to settings
This commit is contained in:
parent
3f1218e7bc
commit
3301af5cbb
@ -35,13 +35,6 @@ export default function Router() {
|
||||
return { Component: HomeScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "nwc",
|
||||
async lazy() {
|
||||
const { NWCScreen } = await import("./routes/nwc");
|
||||
return { Component: NWCScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
element: <SettingsLayout />,
|
||||
@ -100,6 +93,13 @@ export default function Router() {
|
||||
return { Component: AdvancedSettingScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "nwc",
|
||||
async lazy() {
|
||||
const { NWCScreen } = await import("./routes/settings/nwc");
|
||||
return { Component: NWCScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "about",
|
||||
async lazy() {
|
||||
|
@ -1,125 +0,0 @@
|
||||
import { CheckCircleIcon } from "@lume/icons";
|
||||
import { useStorage } from "@lume/storage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NWCForm } from "./components/form";
|
||||
|
||||
export function NWCScreen() {
|
||||
const storage = useStorage();
|
||||
const [walletConnectURL, setWalletConnectURL] = useState<null | string>(null);
|
||||
|
||||
const remove = async () => {
|
||||
await storage.removePrivkey(`${ark.account.pubkey}-nwc`);
|
||||
setWalletConnectURL(null);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function getNWC() {
|
||||
const nwc = await storage.loadPrivkey(`${ark.account.pubkey}-nwc`);
|
||||
if (nwc) setWalletConnectURL(nwc);
|
||||
}
|
||||
getNWC();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-bold leading-tight">
|
||||
Nostr Wallet Connect
|
||||
</h3>
|
||||
<p className="leading-tight text-neutral-600 dark:text-neutral-400">
|
||||
Sending zap easily via Bitcoin Lightning.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto max-w-lg">
|
||||
{!walletConnectURL ? (
|
||||
<NWCForm setWalletConnectURL={setWalletConnectURL} />
|
||||
) : (
|
||||
<div className="flex w-full flex-col gap-3 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||
<div className="flex items-center justify-center gap-1.5 text-sm text-teal-500">
|
||||
<CheckCircleIcon className="h-4 w-4" />
|
||||
<div>You're using nostr wallet connect</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<textarea
|
||||
readOnly
|
||||
value={`${walletConnectURL.substring(0, 120)}****`}
|
||||
className="h-40 w-full resize-none rounded-lg border-transparent bg-neutral-200 px-3 py-3 text-neutral-900 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:focus:ring-blue-800 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove()}
|
||||
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-neutral-200 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:bg-neutral-800 dark:text-neutral-100"
|
||||
>
|
||||
Remove connection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-5 flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Introduction
|
||||
</h5>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Nostr Wallet Connect (NWC) is a way for applications like Nostr
|
||||
clients to access a remote Lightning wallet through a
|
||||
standardized protocol.
|
||||
</p>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
To learn more about the details have a look at{" "}
|
||||
<a
|
||||
href="https://github.com/nostr-protocol/nips/blob/master/47.md"
|
||||
target="_blank"
|
||||
className="text-blue-500"
|
||||
rel="noreferrer"
|
||||
>
|
||||
the specs (NIP47)
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
About zapping
|
||||
</h5>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Lume doesn't take any commission or platform fees when you
|
||||
zap someone.
|
||||
</p>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Lume doesn't hold your Bitcoin
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Recommend wallet that support NWC
|
||||
</h5>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Mutiny Wallet:{" "}
|
||||
<a
|
||||
href="https://www.mutinywallet.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500"
|
||||
>
|
||||
website
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
Self hosted NWC on Umbrel :{" "}
|
||||
<a
|
||||
href="https://apps.umbrel.com/app/alby-nostr-wallet-connect"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500"
|
||||
>
|
||||
website
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { useStorage } from "@lume/storage";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function NWCForm({ setWalletConnectURL }) {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
|
||||
const [uri, setUri] = useState("");
|
154
apps/desktop/src/routes/settings/nwc.tsx
Normal file
154
apps/desktop/src/routes/settings/nwc.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
import { webln } from "@getalby/sdk";
|
||||
import { useArk } from "@lume/ark";
|
||||
import { CheckCircleIcon } from "@lume/icons";
|
||||
import { useStorage } from "@lume/storage";
|
||||
import { useEffect, useState } from "react";
|
||||
import { NWCForm } from "./components/walletConnectForm";
|
||||
|
||||
export function NWCScreen() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
|
||||
const [walletConnectURL, setWalletConnectURL] = useState<null | string>(null);
|
||||
const [balance, setBalance] = useState(0);
|
||||
|
||||
const remove = async () => {
|
||||
await storage.removePrivkey(`${ark.account.pubkey}-nwc`);
|
||||
setWalletConnectURL(null);
|
||||
};
|
||||
|
||||
const loadBalance = async () => {
|
||||
const nwc = new webln.NostrWebLNProvider({
|
||||
nostrWalletConnectUrl: walletConnectURL,
|
||||
});
|
||||
await nwc.enable();
|
||||
|
||||
const balanceResponse = await nwc.getBalance();
|
||||
setBalance(balanceResponse.balance);
|
||||
|
||||
nwc.close();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (walletConnectURL) loadBalance();
|
||||
}, [walletConnectURL]);
|
||||
|
||||
useEffect(() => {
|
||||
async function getNWC() {
|
||||
const nwc = await storage.loadPrivkey(`${ark.account.pubkey}-nwc`);
|
||||
if (nwc) setWalletConnectURL(nwc);
|
||||
}
|
||||
getNWC();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
<div className="text-center">
|
||||
<h3 className="text-2xl font-semibold leading-tight">
|
||||
Nostr Wallet Connect
|
||||
</h3>
|
||||
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
|
||||
Sending zap easily via Bitcoin Lightning.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mx-auto w-full max-w-lg">
|
||||
{!walletConnectURL ? (
|
||||
<NWCForm setWalletConnectURL={setWalletConnectURL} />
|
||||
) : (
|
||||
<div className="flex w-full flex-col gap-3 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||
<div className="flex items-center justify-center gap-1.5 text-sm text-teal-500">
|
||||
<CheckCircleIcon className="h-4 w-4" />
|
||||
<div>You're using nostr wallet connect</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<textarea
|
||||
readOnly
|
||||
value={`${walletConnectURL.substring(0, 120)}****`}
|
||||
className="h-40 w-full resize-none rounded-lg border-transparent bg-neutral-200 px-3 py-3 text-neutral-900 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:focus:ring-blue-800 dark:bg-neutral-800 dark:text-neutral-100 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove()}
|
||||
className="inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-neutral-200 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:bg-neutral-800 dark:text-neutral-100"
|
||||
>
|
||||
Remove connection
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{walletConnectURL ? (
|
||||
<div className="mt-5 flex flex-col">
|
||||
<h3 className="font-medium text-neutral-600 dark:text-neutral-400">
|
||||
Current balance
|
||||
</h3>
|
||||
<p className="text-2xl font-semibold">
|
||||
{new Intl.NumberFormat().format(balance)} sats
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-5 flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Introduction
|
||||
</h5>
|
||||
<p className="text-neutral-600 dark:text-neutral-400">
|
||||
Nostr Wallet Connect (NWC) is a way for applications like
|
||||
Nostr clients to access a remote Lightning wallet through a
|
||||
standardized protocol.
|
||||
</p>
|
||||
<p className="text-neutral-600 dark:text-neutral-400">
|
||||
To learn more about the details have a look at{" "}
|
||||
<a
|
||||
href="https://github.com/nostr-protocol/nips/blob/master/47.md"
|
||||
target="_blank"
|
||||
className="text-blue-500"
|
||||
rel="noreferrer"
|
||||
>
|
||||
the specs (NIP47)
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
About zapping
|
||||
</h5>
|
||||
<p className="text-neutral-600 dark:text-neutral-400">
|
||||
Lume doesn't take any commission or platform fees when
|
||||
you zap someone. Lume doesn't hold your Bitcoin
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h5 className="font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Recommend wallet that support NWC
|
||||
</h5>
|
||||
<p className="text-neutral-600 dark:text-neutral-400">
|
||||
- Mutiny Wallet:{" "}
|
||||
<a
|
||||
href="https://www.mutinywallet.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500"
|
||||
>
|
||||
website
|
||||
</a>
|
||||
</p>
|
||||
<p className="text-neutral-600 dark:text-neutral-400">
|
||||
- Self hosted NWC on Umbrel :{" "}
|
||||
<a
|
||||
href="https://apps.umbrel.com/app/alby-nostr-wallet-connect"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-blue-500"
|
||||
>
|
||||
website
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -103,13 +103,35 @@ export class Ark {
|
||||
}
|
||||
}
|
||||
|
||||
public getCleanPubkey(pubkey: string) {
|
||||
try {
|
||||
let hexstring = pubkey.replace("nostr:", "").split("'")[0];
|
||||
|
||||
if (
|
||||
hexstring.startsWith("npub1") ||
|
||||
hexstring.startsWith("nprofile1") ||
|
||||
hexstring.startsWith("naddr1")
|
||||
) {
|
||||
const decoded = nip19.decode(hexstring);
|
||||
|
||||
if (decoded.type === "nprofile") hexstring = decoded.data.pubkey;
|
||||
if (decoded.type === "npub") hexstring = decoded.data;
|
||||
if (decoded.type === "naddr") hexstring = decoded.data.pubkey;
|
||||
}
|
||||
|
||||
return hexstring;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async getUserProfile(pubkey?: string) {
|
||||
try {
|
||||
const currentUserPubkey = this.account.pubkey;
|
||||
|
||||
// get clean pubkey without any special characters
|
||||
let hexstring = pubkey
|
||||
? pubkey.replace(/[^a-zA-Z0-9]/g, "").replace("nostr:", "")
|
||||
? pubkey.replace("nostr:", "").split("'")[0]
|
||||
: currentUserPubkey;
|
||||
|
||||
if (
|
||||
@ -135,7 +157,7 @@ export class Ark {
|
||||
if (!profile) return null;
|
||||
return profile;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -145,7 +167,7 @@ export class Ark {
|
||||
|
||||
// get clean pubkey without any special characters
|
||||
let hexstring = pubkey
|
||||
? pubkey.replace(/[^a-zA-Z0-9]/g, "").replace("nostr:", "")
|
||||
? pubkey.replace("nostr:", "").split("'")[0]
|
||||
: currentUserPubkey;
|
||||
|
||||
if (
|
||||
@ -164,14 +186,16 @@ export class Ark {
|
||||
pubkey: hexstring,
|
||||
});
|
||||
|
||||
const contacts = [...(await user.follows())].map((user) => user.pubkey);
|
||||
const contacts = [...(await user.follows(undefined, false))].map(
|
||||
(user) => user.pubkey,
|
||||
);
|
||||
|
||||
if (!pubkey || pubkey === this.account.pubkey)
|
||||
this.account.contacts = contacts;
|
||||
|
||||
return contacts;
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,7 +206,7 @@ export class Ark {
|
||||
});
|
||||
return await user.relayList();
|
||||
} catch (e) {
|
||||
throw new Error(e);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ReplyIcon } from "@lume/icons";
|
||||
import { ChatsIcon, ReplyIcon } from "@lume/icons";
|
||||
import { editorAtom, editorValueAtom } from "@lume/utils";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { useSetAtom } from "jotai";
|
||||
@ -38,7 +38,7 @@ export function NoteReply() {
|
||||
}}
|
||||
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
|
||||
>
|
||||
<ReplyIcon className="size-5 group-hover:text-blue-500" />
|
||||
<ChatsIcon className="size-5 group-hover:text-blue-500" />
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
|
@ -30,66 +30,53 @@ export function NoteChild({
|
||||
NOSTR_MENTIONS.some((el) => word.startsWith(el)),
|
||||
);
|
||||
|
||||
if (hashtags.length) {
|
||||
for (const hashtag of hashtags) {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
hashtag,
|
||||
(match, i) => {
|
||||
return <Hashtag key={match + i} tag={hashtag} />;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mentions.length) {
|
||||
for (const mention of mentions) {
|
||||
const address = mention
|
||||
.replace("nostr:", "")
|
||||
.replace("@", "")
|
||||
.replace(/[^a-zA-Z0-9]/g, "");
|
||||
const decoded = nip19.decode(address);
|
||||
|
||||
if (decoded.type === "npub") {
|
||||
try {
|
||||
if (hashtags.length) {
|
||||
for (const hashtag of hashtags) {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
mention,
|
||||
(match, i) => <MentionUser key={match + i} pubkey={decoded.data} />,
|
||||
);
|
||||
}
|
||||
|
||||
if (decoded.type === "nprofile" || decoded.type === "naddr") {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
mention,
|
||||
(match, i) => (
|
||||
<MentionUser key={match + i} pubkey={decoded.data.pubkey} />
|
||||
),
|
||||
hashtag,
|
||||
(match, i) => {
|
||||
return <Hashtag key={match + i} tag={hashtag} />;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mentions.length) {
|
||||
for (const mention of mentions) {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
mention,
|
||||
(match, i) => <MentionUser key={match + i} pubkey={mention} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
/(https?:\/\/\S+)/g,
|
||||
(match, i) => {
|
||||
const url = new URL(match);
|
||||
return (
|
||||
<Link
|
||||
key={match + i}
|
||||
to={url.toString()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="break-all font-normal text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{url.toString()}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return parsedContent;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return parsedContent;
|
||||
}
|
||||
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
/(https?:\/\/\S+)/g,
|
||||
(match, i) => {
|
||||
const url = new URL(match);
|
||||
return (
|
||||
<Link
|
||||
key={match + i}
|
||||
to={url.toString()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="break-all font-normal text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{url.toString()}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return parsedContent;
|
||||
}, [data]);
|
||||
|
||||
if (isLoading) {
|
||||
|
@ -140,60 +140,21 @@ export function NoteContent({
|
||||
|
||||
if (events.length) {
|
||||
for (const event of events) {
|
||||
const address = event
|
||||
.replace("nostr:", "")
|
||||
.replace(/[^a-zA-Z0-9]/g, "");
|
||||
const decoded = nip19.decode(address);
|
||||
|
||||
if (decoded.type === "note") {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
event,
|
||||
(match, i) => (
|
||||
<MentionNote key={match + i} eventId={decoded.data} />
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (decoded.type === "nevent") {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
event,
|
||||
(match, i) => (
|
||||
<MentionNote key={match + i} eventId={decoded.data.id} />
|
||||
),
|
||||
);
|
||||
}
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
event,
|
||||
(match, i) => <MentionNote key={match + i} eventId={event} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mentions.length) {
|
||||
for (const mention of mentions) {
|
||||
const address = mention
|
||||
.replace("nostr:", "")
|
||||
.replace("@", "")
|
||||
.replace(/[^a-zA-Z0-9]/g, "");
|
||||
const decoded = nip19.decode(address);
|
||||
|
||||
if (decoded.type === "npub") {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
mention,
|
||||
(match, i) => (
|
||||
<MentionUser key={match + i} pubkey={decoded.data} />
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (decoded.type === "nprofile" || decoded.type === "naddr") {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
mention,
|
||||
(match, i) => (
|
||||
<MentionUser key={match + i} pubkey={decoded.data.pubkey} />
|
||||
),
|
||||
);
|
||||
}
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
mention,
|
||||
(match, i) => <MentionUser key={match + i} pubkey={mention} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,24 +2,30 @@ import { COL_TYPES } from "@lume/utils";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { memo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useArk } from "../../../hooks/useArk";
|
||||
import { useProfile } from "../../../hooks/useProfile";
|
||||
import { useColumnContext } from "../../column/provider";
|
||||
|
||||
export const MentionUser = memo(function MentionUser({
|
||||
pubkey,
|
||||
}: { pubkey: string }) {
|
||||
const { user } = useProfile(pubkey);
|
||||
const ark = useArk();
|
||||
const cleanPubkey = ark.getCleanPubkey(pubkey);
|
||||
|
||||
const { isLoading, user } = useProfile(pubkey);
|
||||
const { addColumn } = useColumnContext();
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger className="text-blue-500 break-words hover:text-blue-600">
|
||||
{`@${user?.name || user?.displayName || user?.username || "anon"}`}
|
||||
{isLoading
|
||||
? "@anon"
|
||||
: `@${user.name || user.display_name || user.username || "anon"}`}
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content className="left-[50px] z-50 relative flex w-[200px] flex-col overflow-hidden rounded-xl border border-neutral-200 bg-neutral-950 focus:outline-none dark:border-neutral-900">
|
||||
<DropdownMenu.Item asChild>
|
||||
<Link
|
||||
to={`/users/${pubkey}`}
|
||||
to={`/users/${cleanPubkey}`}
|
||||
className="inline-flex items-center h-10 px-4 text-sm text-white hover:bg-neutral-900 focus:outline-none"
|
||||
>
|
||||
View profile
|
||||
@ -31,8 +37,8 @@ export const MentionUser = memo(function MentionUser({
|
||||
onClick={async () =>
|
||||
await addColumn({
|
||||
kind: COL_TYPES.user,
|
||||
title: user?.name || user?.displayName || "",
|
||||
content: pubkey,
|
||||
title: user?.name || user?.displayName || "Profile",
|
||||
content: cleanPubkey,
|
||||
})
|
||||
}
|
||||
className="inline-flex items-center h-10 px-4 text-sm text-white hover:bg-neutral-900 focus:outline-none"
|
||||
|
37
packages/ark/src/components/user/about.tsx
Normal file
37
packages/ark/src/components/user/about.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { cn } from "@lume/utils";
|
||||
import { useUserContext } from "./provider";
|
||||
|
||||
export function UserAbout({ className }: { className?: string }) {
|
||||
const user = useUserContext();
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={cn(
|
||||
"h-4 w-20 bg-black/20 dark:bg-white/20 rounded animate-pulse",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"h-4 w-full bg-black/20 dark:bg-white/20 rounded animate-pulse",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"h-4 w-24 bg-black/20 dark:bg-white/20 rounded animate-pulse",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("select-text break-p", className)}>
|
||||
{user.about || user.bio}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useArk } from "../../hooks/useArk";
|
||||
@ -7,6 +8,8 @@ export function UserFollowButton({
|
||||
className,
|
||||
}: { target: string; className?: string }) {
|
||||
const ark = useArk();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
const toggleFollow = async () => {
|
||||
@ -21,15 +24,27 @@ export function UserFollowButton({
|
||||
|
||||
useEffect(() => {
|
||||
async function status() {
|
||||
setLoading(true);
|
||||
|
||||
const contacts = await ark.getUserContacts();
|
||||
if (contacts?.includes(target)) setFollowed(true);
|
||||
if (contacts?.includes(target)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
status();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<button type="button" onClick={toggleFollow} className={cn("", className)}>
|
||||
{followed ? "Unfollow" : "Follow"}
|
||||
{loading ? (
|
||||
<LoaderIcon className="size-4 animate-spin" />
|
||||
) : followed ? (
|
||||
"Unfollow"
|
||||
) : (
|
||||
"Follow"
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { UserAbout } from "./about";
|
||||
import { UserAvatar } from "./avatar";
|
||||
import { UserFollowButton } from "./followButton";
|
||||
import { UserName } from "./name";
|
||||
@ -13,5 +14,6 @@ export const User = {
|
||||
Name: UserName,
|
||||
NIP05: UserNip05,
|
||||
Time: UserTime,
|
||||
About: UserAbout,
|
||||
Button: UserFollowButton,
|
||||
};
|
||||
|
@ -12,6 +12,7 @@ import NDK, {
|
||||
NDKUser,
|
||||
} from "@nostr-dev-kit/ndk";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import Linkify from "linkify-react";
|
||||
import { normalizeRelayUrlSet } from "nostr-fetch";
|
||||
@ -133,6 +134,8 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
||||
}
|
||||
|
||||
async function initArk() {
|
||||
if (!ndk) await message("Something wrong!", { type: "error" });
|
||||
|
||||
// ark utils
|
||||
const ark = new Ark({ ndk, account: storage.currentUser });
|
||||
|
||||
|
@ -1,18 +1,13 @@
|
||||
import { RepostNote, TextNote, useArk, useProfile } from "@lume/ark";
|
||||
import { RepostNote, TextNote, User, useArk } from "@lume/ark";
|
||||
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
||||
import { NIP05 } from "@lume/ui";
|
||||
import { FETCH_LIMIT, displayNpub } from "@lume/utils";
|
||||
import { FETCH_LIMIT } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { useMemo } from "react";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
|
||||
export function HomeRoute({ id }: { id: string }) {
|
||||
const ark = useArk();
|
||||
|
||||
const { user } = useProfile(id);
|
||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ["user-posts", id],
|
||||
@ -44,37 +39,11 @@ export function HomeRoute({ id }: { id: string }) {
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
const allEvents = useMemo(
|
||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const follow = async (pubkey: string) => {
|
||||
try {
|
||||
const add = await ark.createContact({ pubkey });
|
||||
if (add) {
|
||||
setFollowed(true);
|
||||
} else {
|
||||
toast.success("You already follow this user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const unfollow = async (pubkey: string) => {
|
||||
try {
|
||||
const remove = await ark.deleteContact({ pubkey });
|
||||
if (remove) {
|
||||
setFollowed(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = (event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
@ -86,76 +55,31 @@ export function HomeRoute({ id }: { id: string }) {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (ark.account.contacts.includes(id)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="py-5 overflow-y-auto">
|
||||
<WindowVirtualizer>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<img
|
||||
src={user?.picture || user?.image}
|
||||
alt={id}
|
||||
className="object-cover w-12 h-12 rounded-lg shrink-0"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{followed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unfollow(id)}
|
||||
className="inline-flex items-center justify-center text-sm font-medium rounded-lg h-9 w-28 bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => follow(id)}
|
||||
className="inline-flex items-center justify-center text-sm font-medium rounded-lg h-9 w-28 bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/chats/${id}`}
|
||||
className="inline-flex items-center justify-center text-sm font-medium rounded-lg h-9 w-28 bg-neutral-200 hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
<User.Provider pubkey={id}>
|
||||
<User.Root className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<User.Avatar className="h-12 w-12 shrink-0 rounded-lg object-cover" />
|
||||
<User.Button
|
||||
target={id}
|
||||
className="inline-flex items-center justify-center w-24 text-sm font-semibold border-t rounded-lg border-neutral-900 dark:border-neutral-800 h-9 bg-neutral-950 text-neutral-50 dark:bg-neutral-900 hover:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-lg font-semibold">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
"Anon"}
|
||||
</h5>
|
||||
{user?.nip05 ? (
|
||||
<NIP05
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<User.Name className="text-lg font-semibold" />
|
||||
<User.NIP05
|
||||
pubkey={id}
|
||||
nip05={user?.nip05}
|
||||
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
) : (
|
||||
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{displayNpub(id, 16)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<User.About className="text-neutral-900 dark:text-neutral-100" />
|
||||
</div>
|
||||
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Latest posts
|
||||
|
@ -1,6 +1,8 @@
|
||||
import {
|
||||
AdvancedSettingsIcon,
|
||||
InfoIcon,
|
||||
NwcFilledIcon,
|
||||
NwcIcon,
|
||||
SecureIcon,
|
||||
SettingsIcon,
|
||||
UserIcon,
|
||||
@ -25,7 +27,7 @@ export function SettingsLayout() {
|
||||
)
|
||||
}
|
||||
>
|
||||
<UserIcon className="h-6 w-6" />
|
||||
<UserIcon className="size-6" />
|
||||
<p className="text-sm font-medium">User</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
@ -39,9 +41,23 @@ export function SettingsLayout() {
|
||||
)
|
||||
}
|
||||
>
|
||||
<SettingsIcon className="h-6 w-6" />
|
||||
<SettingsIcon className="size-6" />
|
||||
<p className="text-sm font-medium">General</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/nwc"
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
"flex w-20 shrink-0 flex-col items-center justify-center rounded-lg px-2 py-2 text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-900",
|
||||
isActive
|
||||
? "bg-neutral-100 text-blue-500 hover:bg-neutral-100 dark:bg-neutral-950 dark:hover:bg-neutral-900"
|
||||
: "",
|
||||
)
|
||||
}
|
||||
>
|
||||
<NwcIcon className="size-6" />
|
||||
<p className="text-sm font-medium">Wallet</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/settings/backup"
|
||||
className={({ isActive }) =>
|
||||
@ -53,7 +69,7 @@ export function SettingsLayout() {
|
||||
)
|
||||
}
|
||||
>
|
||||
<SecureIcon className="h-6 w-6" />
|
||||
<SecureIcon className="size-6" />
|
||||
<p className="text-sm font-medium">Backup</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
@ -67,7 +83,7 @@ export function SettingsLayout() {
|
||||
)
|
||||
}
|
||||
>
|
||||
<AdvancedSettingsIcon className="h-6 w-6" />
|
||||
<AdvancedSettingsIcon className="size-6" />
|
||||
<p className="text-sm font-medium">Advanced</p>
|
||||
</NavLink>
|
||||
<NavLink
|
||||
@ -81,7 +97,7 @@ export function SettingsLayout() {
|
||||
)
|
||||
}
|
||||
>
|
||||
<InfoIcon className="h-6 w-6" />
|
||||
<InfoIcon className="size-6" />
|
||||
<p className="text-sm font-medium">About</p>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
@ -7,9 +7,6 @@ import {
|
||||
DepotIcon,
|
||||
HomeFilledIcon,
|
||||
HomeIcon,
|
||||
NwcIcon,
|
||||
RelayFilledIcon,
|
||||
RelayIcon,
|
||||
SettingsFilledIcon,
|
||||
SettingsIcon,
|
||||
} from "@lume/icons";
|
||||
|
@ -1,25 +1,22 @@
|
||||
import { RepostNote, TextNote, useArk, useProfile } from "@lume/ark";
|
||||
import { RepostNote, TextNote, User, useArk } from "@lume/ark";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
ArrowRightCircleIcon,
|
||||
ArrowRightIcon,
|
||||
LoaderIcon,
|
||||
} from "@lume/icons";
|
||||
import { FETCH_LIMIT, displayNpub } from "@lume/utils";
|
||||
import { FETCH_LIMIT } from "@lume/utils";
|
||||
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { useMemo } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { WindowVirtualizer } from "virtua";
|
||||
import { NIP05 } from "../nip05";
|
||||
|
||||
export function UserRoute() {
|
||||
const ark = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { id } = useParams();
|
||||
const { user } = useProfile(id);
|
||||
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
|
||||
useInfiniteQuery({
|
||||
queryKey: ["user-posts", id],
|
||||
@ -51,37 +48,11 @@ export function UserRoute() {
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
const allEvents = useMemo(
|
||||
() => (data ? data.pages.flatMap((page) => page) : []),
|
||||
[data],
|
||||
);
|
||||
|
||||
const follow = async (pubkey: string) => {
|
||||
try {
|
||||
const add = await ark.createContact({ pubkey });
|
||||
if (add) {
|
||||
setFollowed(true);
|
||||
} else {
|
||||
toast.success("You already follow this user");
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const unfollow = async (pubkey: string) => {
|
||||
try {
|
||||
const remove = await ark.deleteContact({ pubkey });
|
||||
if (remove) {
|
||||
setFollowed(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderItem = (event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
@ -93,12 +64,6 @@ export function UserRoute() {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (ark.account.contacts.includes(id)) {
|
||||
setFollowed(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="pb-5 overflow-y-auto">
|
||||
<WindowVirtualizer>
|
||||
@ -119,66 +84,27 @@ export function UserRoute() {
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-3">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<img
|
||||
src={user?.picture || user?.image}
|
||||
alt={id}
|
||||
className="h-12 w-12 shrink-0 rounded-lg object-cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{followed ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => unfollow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Unfollow
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => follow(id)}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
)}
|
||||
<Link
|
||||
to={`/chats/${id}`}
|
||||
className="inline-flex h-9 w-28 items-center justify-center rounded-lg bg-neutral-200 text-sm font-medium hover:bg-blue-500 hover:text-white dark:bg-neutral-800"
|
||||
>
|
||||
Message
|
||||
</Link>
|
||||
<User.Provider pubkey={id}>
|
||||
<User.Root className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<User.Avatar className="h-12 w-12 shrink-0 rounded-lg object-cover" />
|
||||
<User.Button
|
||||
target={id}
|
||||
className="inline-flex items-center justify-center w-24 text-sm font-semibold border-t rounded-lg border-neutral-900 dark:border-neutral-800 h-9 bg-neutral-950 text-neutral-50 dark:bg-neutral-900 hover:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<h5 className="text-lg font-semibold">
|
||||
{user?.name ||
|
||||
user?.display_name ||
|
||||
user?.displayName ||
|
||||
"Anon"}
|
||||
</h5>
|
||||
{user?.nip05 ? (
|
||||
<NIP05
|
||||
<div className="flex flex-1 flex-col gap-1.5">
|
||||
<div className="flex flex-col">
|
||||
<User.Name className="text-lg font-semibold" />
|
||||
<User.NIP05
|
||||
pubkey={id}
|
||||
nip05={user?.nip05}
|
||||
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
|
||||
/>
|
||||
) : (
|
||||
<span className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{displayNpub(id, 16)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<User.About className="text-neutral-900 dark:text-neutral-100" />
|
||||
</div>
|
||||
<div className="max-w-[500px] select-text break-words text-neutral-900 dark:text-neutral-100">
|
||||
{user?.about}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
|
||||
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
Latest posts
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { atom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
|
||||
// Editor
|
||||
export const editorAtom = atom(false);
|
||||
@ -17,4 +18,4 @@ export const activityAtom = atom(false);
|
||||
export const activityUnreadAtom = atom(0);
|
||||
|
||||
// Tutorial
|
||||
export const tutorialAtom = atom(true);
|
||||
export const tutorialAtom = atomWithStorage("tutorial", true);
|
||||
|
Loading…
Reference in New Issue
Block a user