Merge pull request #147 from luminous-devs/feat/improve-design

Improve overal design
This commit is contained in:
Ren Amamiya
2024-01-26 14:21:27 +07:00
committed by GitHub
71 changed files with 1321 additions and 1078 deletions

View File

@@ -25,6 +25,7 @@
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
@@ -33,9 +34,9 @@
"@radix-ui/react-select": "^2.0.0", "@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.17.15", "@tanstack/react-query": "^5.17.19",
"framer-motion": "^10.18.0", "framer-motion": "^10.18.0",
"jotai": "^2.6.2", "jotai": "^2.6.3",
"minidenticons": "^4.2.0", "minidenticons": "^4.2.0",
"nanoid": "^5.0.4", "nanoid": "^5.0.4",
"nostr-fetch": "^0.15.0", "nostr-fetch": "^0.15.0",
@@ -45,15 +46,15 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.49.3", "react-hook-form": "^7.49.3",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
"smol-toml": "^1.1.3", "smol-toml": "^1.1.4",
"sonner": "^1.3.1", "sonner": "^1.3.1",
"virtua": "^0.20.5" "virtua": "^0.21.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^", "@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^", "@lume/types": "workspace:^",
"@types/node": "^20.11.5", "@types/node": "^20.11.6",
"@types/react": "^18.2.48", "@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"@vitejs/plugin-react-swc": "^3.5.0", "@vitejs/plugin-react-swc": "^3.5.0",
@@ -63,7 +64,7 @@
"postcss": "^8.4.33", "postcss": "^8.4.33",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.0.11", "vite": "^5.0.12",
"vite-plugin-top-level-await": "^1.4.1", "vite-plugin-top-level-await": "^1.4.1",
"vite-tsconfig-paths": "^4.3.1" "vite-tsconfig-paths": "^4.3.1"
} }

View File

@@ -12,6 +12,10 @@
.prose :where(iframe):not(:where([class~='not-prose'] *)) { .prose :where(iframe):not(:where([class~='not-prose'] *)) {
@apply w-full h-auto mx-auto aspect-video; @apply w-full h-auto mx-auto aspect-video;
} }
.shadow-toolbar {
box-shadow: 0 0 #0000,0 0 #0000,0 8px 24px 0 rgba(0,0,0,.2),0 2px 8px 0 rgba(0,0,0,.08),inset 0 0 0 1px rgba(0,0,0,.2),inset 0 0 0 2px hsla(0,0%,100%,.14)
}
} }
html { html {

View File

@@ -207,9 +207,6 @@ export default function Router() {
}, },
{ {
path: "create", path: "create",
loader: async () => {
return await ark.getOAuthServices();
},
async lazy() { async lazy() {
const { CreateAccountScreen } = await import( const { CreateAccountScreen } = await import(
"./routes/auth/create" "./routes/auth/create"
@@ -217,6 +214,27 @@ export default function Router() {
return { Component: CreateAccountScreen }; return { Component: CreateAccountScreen };
}, },
}, },
{
path: "create-keys",
async lazy() {
const { CreateAccountKeys } = await import(
"./routes/auth/create-keys"
);
return { Component: CreateAccountKeys };
},
},
{
path: "create-address",
loader: async () => {
return await ark.getOAuthServices();
},
async lazy() {
const { CreateAccountAddress } = await import(
"./routes/auth/create-address"
);
return { Component: CreateAccountAddress };
},
},
{ {
path: "login", path: "login",
async lazy() { async lazy() {

View File

@@ -0,0 +1,263 @@
import { useArk } from "@lume/ark";
import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { onboardingAtom } from "@lume/utils";
import NDK, {
NDKEvent,
NDKKind,
NDKNip46Signer,
NDKPrivateKeySigner,
} from "@nostr-dev-kit/ndk";
import * as Select from "@radix-ui/react-select";
import { UnlistenFn } from "@tauri-apps/api/event";
import { Window } from "@tauri-apps/api/window";
import { useSetAtom } from "jotai";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { useLoaderData, useNavigate } from "react-router-dom";
import { toast } from "sonner";
const Item = ({ event }: { event: NDKEvent }) => {
const domain = JSON.parse(event.content).nip05.replace("_@", "");
return (
<Select.Item
value={event.id}
className="relative flex items-center pr-10 leading-none rounded-md select-none text-neutral-100 rounded-mg h-9 pl-7"
>
<Select.ItemText>@{domain}</Select.ItemText>
<Select.ItemIndicator className="absolute left-0 inline-flex items-center justify-center transform h-7">
<CheckIcon className="size-4" />
</Select.ItemIndicator>
</Select.Item>
);
};
export function CreateAccountAddress() {
const ark = useArk();
const storage = useStorage();
const services = useLoaderData() as NDKEvent[];
const setOnboarding = useSetAtom(onboardingAtom);
const navigate = useNavigate();
const [serviceId, setServiceId] = useState(services?.[0]?.id);
const [loading, setIsLoading] = useState(false);
const {
register,
handleSubmit,
formState: { isValid },
} = useForm();
const getDomainName = (id: string) => {
const event = services.find((ev) => ev.id === id);
return JSON.parse(event.content).nip05.replace("_@", "") as string;
};
const onSubmit = async (data: { username: string; email: string }) => {
try {
setIsLoading(true);
const domain = getDomainName(serviceId);
const service = services.find((ev) => ev.id === serviceId);
// generate ndk for nsecbunker
const localSigner = NDKPrivateKeySigner.generate();
const bunker = new NDK({
explicitRelayUrls: [
"wss://relay.nsecbunker.com/",
"wss://nostr.vulpem.com/",
],
});
await bunker.connect(2000);
// generate tmp remote singer for create account
const remoteSigner = new NDKNip46Signer(
bunker,
service.pubkey,
localSigner,
);
// handle auth url request
let unlisten: UnlistenFn;
let authWindow: Window;
let account: string = undefined;
remoteSigner.addListener("authUrl", async (authUrl: string) => {
authWindow = new Window(`auth-${serviceId}`, {
url: authUrl,
title: domain,
titleBarStyle: "overlay",
width: 600,
height: 650,
center: true,
closable: false,
});
unlisten = await authWindow.onCloseRequested(() => {
if (!account) {
setIsLoading(false);
unlisten();
return authWindow.close();
}
});
});
// create new account
account = await remoteSigner.createAccount(
data.username,
domain,
data.email,
);
if (!account) {
unlisten();
setIsLoading(false);
authWindow.close();
return toast.error("Failed to create new account, try again later");
}
unlisten();
authWindow.close();
// add account to storage
await storage.createSetting("nsecbunker", "1");
const newAccount = await storage.createAccount({
pubkey: account,
privkey: localSigner.privateKey,
});
ark.account = newAccount;
// get final signer with newly created account
const finalSigner = new NDKNip46Signer(bunker, account, localSigner);
await finalSigner.blockUntilReady();
// update main ndk instance signer
ark.updateNostrSigner({ signer: finalSigner });
// remove default nsecbunker profile and contact list
// await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] });
await ark.createEvent({ kind: NDKKind.Contacts, content: "", tags: [] });
setIsLoading(false);
setOnboarding({ open: true, newUser: true });
return navigate("/auth/onboarding", { replace: true });
} catch (e) {
setIsLoading(false);
toast.error(String(e));
}
};
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">
Let's set up your account on Nostr
</h1>
</div>
{!services ? (
<div className="flex items-center justify-center w-full">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : (
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-3 mb-0"
>
<div className="flex flex-col gap-6 p-5 bg-neutral-950 rounded-2xl">
<div className="flex flex-col gap-2">
<label
htmlFor="username"
className="text-sm font-semibold uppercase text-neutral-600"
>
Username *
</label>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between w-full gap-2 bg-neutral-900 rounded-xl">
<input
type={"text"}
{...register("username", {
required: true,
minLength: 1,
})}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="alice"
className="flex-1 min-w-0 text-xl bg-transparent border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-14 ring-0 placeholder:text-neutral-600"
/>
<Select.Root value={serviceId} onValueChange={setServiceId}>
<Select.Trigger className="inline-flex items-center justify-end gap-2 pr-3 text-xl font-semibold text-blue-500 w-max shrink-0">
<Select.Value>@{getDomainName(serviceId)}</Select.Value>
<Select.Icon>
<ChevronDownIcon className="size-5" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="rounded-lg border border-white/20 bg-white/10 backdrop-blur-xl">
<Select.Viewport className="p-3">
<Select.Group>
<Select.Label className="mb-2 text-sm font-medium uppercase px-7 text-neutral-600">
Choose a Provider
</Select.Label>
{services.map((service) => (
<Item key={service.id} event={service} />
))}
</Select.Group>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
<span className="text-sm text-neutral-600">
Use to login to Lume and other Nostr apps. You can choose
provider you trust to manage your account
</span>
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="flex flex-col gap-2">
<label
htmlFor="email"
className="text-sm font-semibold uppercase text-neutral-600"
>
Backup Email (optional)
</label>
<input
type={"email"}
{...register("email", { required: false })}
spellCheck={false}
autoCapitalize="none"
autoCorrect="none"
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/>
</div>
<span className="text-sm text-neutral-600">
Use for recover your account if you lose your password
</span>
</div>
</div>
<div>
<button
type="submit"
disabled={!isValid}
className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Continue"
)}
</button>
</div>
</form>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,186 @@
import { useArk } from "@lume/ark";
import { CheckIcon, EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { onboardingAtom } from "@lume/utils";
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import * as Checkbox from "@radix-ui/react-checkbox";
import { desktopDir } from "@tauri-apps/api/path";
import { save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs";
import { useSetAtom } from "jotai";
import { nanoid } from "nanoid";
import { getPublicKey, nip19 } from "nostr-tools";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
export function CreateAccountKeys() {
const ark = useArk();
const storage = useStorage();
const setOnboarding = useSetAtom(onboardingAtom);
const navigate = useNavigate();
const [key, setKey] = useState("");
const [loading, setLoading] = useState(false);
const [showKey, setShowKey] = useState(false);
const [confirm, setConfirm] = useState({ c1: false, c2: false, c3: false });
const submit = async () => {
try {
setLoading(true);
const privkey = nip19.decode(key).data as string;
const signer = new NDKPrivateKeySigner(privkey);
const pubkey = getPublicKey(privkey);
ark.updateNostrSigner({ signer });
const downloadPath = await desktopDir();
const fileName = `nostr_keys_${nanoid(4)}.txt`;
const filePath = await save({
defaultPath: `${downloadPath}/${fileName}`,
});
if (!filePath) {
return toast.info("You need to save account keys before continue.");
}
await writeTextFile(
filePath,
`Nostr Account\nGenerated by Lume (lume.nu)\n---\nPrivate key: ${key}`,
);
const newAccount = await storage.createAccount({
pubkey: pubkey,
privkey: privkey,
});
ark.account = newAccount;
setLoading(false);
setOnboarding({ open: true, newUser: true });
return navigate("/auth/onboarding", { replace: true });
} catch (e) {
setLoading(false);
toast.error(String(e));
}
};
useEffect(() => {
const privkey = NDKPrivateKeySigner.generate().privateKey;
setKey(nip19.nsecEncode(privkey));
}, []);
return (
<div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">
This is your new Account Key
</h1>
<p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
Keep your key in safe place. If you lose this key, you will lose
access to your account.
</p>
</div>
<div className="flex flex-col gap-6 mb-0">
<div className="flex flex-col gap-6">
<div className="relative">
<input
readOnly
value={key}
type={showKey ? "text" : "password"}
className="pl-3 pr-14 w-full resize-none text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/>
<button
type="button"
onClick={() => setShowKey((state) => !state)}
className="absolute right-2 top-2 size-10 inline-flex items-center justify-center rounded-lg text-white bg-neutral-800 hover:bg-neutral-700"
>
{showKey ? (
<EyeOnIcon className="size-5" />
) : (
<EyeOffIcon className="size-5" />
)}
</button>
</div>
<div className="flex flex-col gap-3">
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c1}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c1: !state.c1 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-900 outline-none"
id="confirm1"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm leading-none text-neutral-500"
htmlFor="confirm1"
>
I understand the risk of lost private key.
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c2}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c2: !state.c2 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-900 outline-none"
id="confirm2"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm leading-none text-neutral-500"
htmlFor="confirm2"
>
I will make sure keep it safe and not sharing with anyone.
</label>
</div>
<div className="flex items-center gap-2">
<Checkbox.Root
checked={confirm.c3}
onCheckedChange={() =>
setConfirm((state) => ({ ...state, c3: !state.c3 }))
}
className="flex size-7 appearance-none items-center justify-center rounded-lg bg-neutral-900 outline-none"
id="confirm3"
>
<Checkbox.Indicator className="text-blue-500">
<CheckIcon className="size-4" />
</Checkbox.Indicator>
</Checkbox.Root>
<label
className="text-sm leading-none text-neutral-500"
htmlFor="confirm3"
>
I understand I cannot recover private key.
</label>
</div>
</div>
</div>
<button
type="button"
onClick={submit}
disabled={!confirm.c1 || !confirm.c2 || !confirm.c3}
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
"Save key & Continue"
)}
</button>
</div>
</div>
</div>
);
}

View File

@@ -1,192 +1,21 @@
import { useArk } from "@lume/ark"; import { LoaderIcon } from "@lume/icons";
import { CheckIcon, ChevronDownIcon, LoaderIcon } from "@lume/icons"; import { cn } from "@lume/utils";
import { useStorage } from "@lume/storage";
import { onboardingAtom } from "@lume/utils";
import NDK, {
NDKEvent,
NDKKind,
NDKNip46Signer,
NDKPrivateKeySigner,
} from "@nostr-dev-kit/ndk";
import * as Select from "@radix-ui/react-select";
import { UnlistenFn } from "@tauri-apps/api/event";
import { desktopDir } from "@tauri-apps/api/path";
import { Window } from "@tauri-apps/api/window";
import { save } from "@tauri-apps/plugin-dialog";
import { writeTextFile } from "@tauri-apps/plugin-fs";
import { useSetAtom } from "jotai";
import { nanoid } from "nanoid";
import { getPublicKey, nip19 } from "nostr-tools";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { Link, useNavigate } from "react-router-dom";
import { useLoaderData, useNavigate } from "react-router-dom";
import { toast } from "sonner";
const Item = ({ event }: { event: NDKEvent }) => {
const domain = JSON.parse(event.content).nip05.replace("_@", "");
return (
<Select.Item
value={event.id}
className="relative flex items-center pr-10 leading-none rounded-md select-none text-neutral-100 rounded-mg h-9 pl-7"
>
<Select.ItemText>@{domain}</Select.ItemText>
<Select.ItemIndicator className="absolute left-0 inline-flex items-center justify-center transform h-7">
<CheckIcon className="size-4" />
</Select.ItemIndicator>
</Select.Item>
);
};
export function CreateAccountScreen() { export function CreateAccountScreen() {
const ark = useArk();
const storage = useStorage();
const services = useLoaderData() as NDKEvent[];
const setOnboarding = useSetAtom(onboardingAtom);
const navigate = useNavigate(); const navigate = useNavigate();
const [serviceId, setServiceId] = useState(services?.[0]?.id); const [method, setMethod] = useState<"self" | "managed">("self");
const [loading, setIsLoading] = useState(false); const [loading, setLoading] = useState(false);
const { const next = () => {
register, setLoading(true);
handleSubmit,
formState: { isValid },
} = useForm();
const getDomainName = (id: string) => { if (method === "self") {
const event = services.find((ev) => ev.id === id); navigate("/auth/create-keys");
return JSON.parse(event.content).nip05.replace("_@", "") as string; } else {
}; navigate("/auth/create-address");
const generateNostrKeys = async () => {
const signer = NDKPrivateKeySigner.generate();
const pubkey = getPublicKey(signer.privateKey);
const npub = nip19.npubEncode(pubkey);
const nsec = nip19.nsecEncode(signer.privateKey);
ark.updateNostrSigner({ signer });
const downloadPath = await desktopDir();
const fileName = `nostr_keys_${nanoid(4)}.txt`;
const filePath = await save({
defaultPath: `${downloadPath}/${fileName}`,
});
if (!filePath) {
return toast.info("You need to save account keys before continue.");
}
await writeTextFile(
filePath,
`Nostr Account\nGenerated by Lume (lume.nu)\n---\nPublic key: ${npub}\nPrivate key: ${nsec}`,
);
await storage.createAccount({
pubkey: pubkey,
privkey: signer.privateKey,
});
setOnboarding({ open: true, newUser: true });
return navigate("/auth/onboarding", { replace: true });
};
const onSubmit = async (data: { username: string; email: string }) => {
try {
setIsLoading(true);
const domain = getDomainName(serviceId);
const service = services.find((ev) => ev.id === serviceId);
// generate ndk for nsecbunker
const localSigner = NDKPrivateKeySigner.generate();
const bunker = new NDK({
explicitRelayUrls: [
"wss://relay.nsecbunker.com/",
"wss://nostr.vulpem.com/",
],
});
await bunker.connect(2000);
// generate tmp remote singer for create account
const remoteSigner = new NDKNip46Signer(
bunker,
service.pubkey,
localSigner,
);
// handle auth url request
let unlisten: UnlistenFn;
let authWindow: Window;
let account: string = undefined;
remoteSigner.addListener("authUrl", async (authUrl: string) => {
authWindow = new Window(`auth-${serviceId}`, {
url: authUrl,
title: domain,
titleBarStyle: "overlay",
width: 600,
height: 650,
center: true,
closable: false,
});
unlisten = await authWindow.onCloseRequested(() => {
if (!account) {
setIsLoading(false);
unlisten();
return authWindow.close();
}
});
});
// create new account
account = await remoteSigner.createAccount(
data.username,
domain,
data.email,
);
if (!account) {
unlisten();
setIsLoading(false);
authWindow.close();
return toast.error("Failed to create new account, try again later");
}
unlisten();
authWindow.close();
// add account to storage
await storage.createSetting("nsecbunker", "1");
const dbAccount = await storage.createAccount({
pubkey: account,
privkey: localSigner.privateKey,
});
ark.account = dbAccount;
// get final signer with newly created account
const finalSigner = new NDKNip46Signer(bunker, account, localSigner);
await finalSigner.blockUntilReady();
// update main ndk instance signer
ark.updateNostrSigner({ signer: finalSigner });
// remove default nsecbunker profile and contact list
// await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] });
await ark.createEvent({ kind: NDKKind.Contacts, content: "", tags: [] });
setIsLoading(false);
setOnboarding({ open: true, newUser: true });
return navigate("/auth/onboarding", { replace: true });
} catch (e) {
setIsLoading(false);
toast.error(String(e));
} }
}; };
@@ -194,148 +23,50 @@ export function CreateAccountScreen() {
<div className="relative flex items-center justify-center w-full h-full"> <div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto"> <div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold"> <h1 className="text-2xl font-semibold">Let's Get Started</h1>
Let's get you set up on Nostr. <p className="text-lg font-medium leading-snug text-neutral-600 dark:text-neutral-500">
</h1> Choose one of methods below to create your account
</p>
</div> </div>
{!services ? ( <div className="flex flex-col gap-4">
<div className="flex items-center justify-center w-full">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : (
<div className="flex flex-col gap-6">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex flex-col gap-3 mb-0"
>
<div className="flex flex-col gap-6 p-5 bg-neutral-950 rounded-2xl">
<div className="flex flex-col gap-2">
<label
htmlFor="username"
className="text-sm font-semibold uppercase text-neutral-600"
>
Username *
</label>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between w-full gap-2 bg-neutral-900 rounded-xl">
<input
type={"text"}
{...register("username", {
required: true,
minLength: 1,
})}
spellCheck={false}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
placeholder="alice"
className="flex-1 min-w-0 text-xl bg-transparent border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-14 ring-0 placeholder:text-neutral-600"
/>
<Select.Root
value={serviceId}
onValueChange={setServiceId}
>
<Select.Trigger className="inline-flex items-center justify-end gap-2 pr-3 text-xl font-semibold text-blue-500 w-max shrink-0">
<Select.Value>
@{getDomainName(serviceId)}
</Select.Value>
<Select.Icon>
<ChevronDownIcon className="size-5" />
</Select.Icon>
</Select.Trigger>
<Select.Portal>
<Select.Content className="rounded-lg border border-white/20 bg-white/10 backdrop-blur-xl">
<Select.Viewport className="p-3">
<Select.Group>
<Select.Label className="mb-2 text-sm font-medium uppercase px-7 text-neutral-600">
Choose a Provider
</Select.Label>
{services.map((service) => (
<Item key={service.id} event={service} />
))}
</Select.Group>
</Select.Viewport>
</Select.Content>
</Select.Portal>
</Select.Root>
</div>
<span className="text-sm text-neutral-600">
Use to login to Lume and other Nostr apps. You can choose
provider you trust to manage your account
</span>
</div>
</div>
<div className="flex flex-col gap-1.5">
<div className="flex flex-col gap-2">
<label
htmlFor="email"
className="text-sm font-semibold uppercase text-neutral-600"
>
Backup Email (optional)
</label>
<input
type={"email"}
{...register("email", { required: false })}
spellCheck={false}
autoCapitalize="none"
autoCorrect="none"
className="px-3 text-xl border-transparent rounded-xl h-14 bg-neutral-900 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-800"
/>
</div>
<span className="text-sm text-neutral-600">
Use for recover your account if you lose your password
</span>
</div>
</div>
<div>
<button <button
type="submit" type="button"
disabled={!isValid} onClick={() => setMethod("self")}
className="inline-flex items-center justify-center w-full text-lg h-12 font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600 disabled:opacity-50" className={cn(
"flex flex-col items-start px-4 py-3.5 bg-neutral-900 rounded-xl hover:bg-neutral-800",
method === "self" ? "ring-1 ring-teal-500" : "",
)}
>
<p className="font-semibold">Self-Managed</p>
<p className="text-sm font-medium text-neutral-500">
You create your keys and keep them safe.
</p>
</button>
<button
type="button"
onClick={() => setMethod("managed")}
className={cn(
"flex flex-col items-start px-4 py-3.5 bg-neutral-900 rounded-xl hover:bg-neutral-800",
method === "managed" ? "ring-1 ring-teal-500" : "",
)}
>
<p className="font-semibold">Managed by Provider</p>
<p className="text-sm font-medium text-neutral-500">
A 3rd party provider will handle your sign in keys for you.
</p>
</button>
<button
type="button"
onClick={next}
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
> >
{loading ? ( {loading ? (
<LoaderIcon className="size-5 animate-spin" /> <LoaderIcon className="size-5 animate-spin" />
) : ( ) : (
"Create Account" "Continue"
)} )}
</button> </button>
</div> </div>
</form>
<div className="flex flex-col gap-6">
<div className="flex flex-col">
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-neutral-900" />
</div>
<div className="relative flex justify-center">
<span className="px-2 font-medium bg-black text-neutral-500">
Or manage your own keys
</span>
</div>
</div>
<span className="mx-auto text-xs font-medium bg-black text-neutral-600">
Mostly compatible with other Nostr clients
</span>
</div>
<div>
<button
type="button"
onClick={generateNostrKeys}
className="mb-2 inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
>
Generate Nostr Keys
</button>
<p className="text-sm text-center text-neutral-500">
If you are using this option, please make sure keep your keys
in safe place. You{" "}
<span className="text-red-600">cannot recover</span> if it
lost, all your data will be{" "}
<span className="text-red-600">lost forever.</span>
</p>
</div>
</div>
</div>
)}
</div> </div>
</div> </div>
); );

View File

@@ -130,7 +130,7 @@ export function LoginWithOAuth() {
<div className="relative flex items-center justify-center w-full h-full"> <div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto"> <div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold">Enter your NIP-05 address</h1> <h1 className="text-2xl font-semibold">Enter your Nostr Address</h1>
</div> </div>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<form <form

View File

@@ -5,9 +5,7 @@ export function LoginScreen() {
<div className="relative flex items-center justify-center w-full h-full"> <div className="relative flex items-center justify-center w-full h-full">
<div className="flex flex-col w-full max-w-md gap-8 mx-auto"> <div className="flex flex-col w-full max-w-md gap-8 mx-auto">
<div className="flex flex-col gap-1 text-center items-center"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold"> <h1 className="text-2xl font-semibold">Welcome back, anon!</h1>
Continue your experience on Nostr
</h1>
</div> </div>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@@ -15,13 +13,13 @@ export function LoginScreen() {
to="/auth/login-oauth" to="/auth/login-oauth"
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600" className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
> >
Login with Address Login with Nostr Address
</Link> </Link>
<Link <Link
to="/auth/login-nsecbunker" to="/auth/login-nsecbunker"
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900" className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
> >
Login with nsecbunker Login with nsecBunker
</Link> </Link>
</div> </div>
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
@@ -31,7 +29,7 @@ export function LoginScreen() {
</div> </div>
<div className="relative flex justify-center"> <div className="relative flex justify-center">
<span className="px-2 font-medium bg-black text-neutral-600"> <span className="px-2 font-medium bg-black text-neutral-600">
Or (Not recommended) Or continue with
</span> </span>
</div> </div>
</div> </div>
@@ -43,8 +41,10 @@ export function LoginScreen() {
Login with Private Key Login with Private Key
</Link> </Link>
<p className="text-sm text-center text-neutral-500"> <p className="text-sm text-center text-neutral-500">
Lume will store your Private Key in{" "} Lume will put your Private Key in{" "}
<span className="text-teal-600">OS Secure Storage</span> <span className="text-teal-600">Secure Storage</span> depended
on your OS Platform. It will be secured by Password or Biometric
ID
</p> </p>
</div> </div>
</div> </div>

View File

@@ -2,10 +2,7 @@ import { useArk } from "@lume/ark";
import { InfoIcon, LoaderIcon } from "@lume/icons"; import { InfoIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage"; import { useStorage } from "@lume/storage";
import { TranslateRegisterModal } from "@lume/ui"; import { TranslateRegisterModal } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKKind } from "@nostr-dev-kit/ndk";
import * as Switch from "@radix-ui/react-switch"; import * as Switch from "@radix-ui/react-switch";
import { useQueryClient } from "@tanstack/react-query";
import { import {
isPermissionGranted, isPermissionGranted,
requestPermission, requestPermission,
@@ -17,7 +14,6 @@ import { toast } from "sonner";
export function OnboardingScreen() { export function OnboardingScreen() {
const ark = useArk(); const ark = useArk();
const storage = useStorage(); const storage = useStorage();
const queryClient = useQueryClient();
const navigate = useNavigate(); const navigate = useNavigate();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@@ -61,32 +57,7 @@ export function OnboardingScreen() {
// get account contacts // get account contacts
await ark.getUserContacts(); await ark.getUserContacts();
// refetch newsfeed navigate("/", { replace: true });
await queryClient.prefetchInfiniteQuery({
queryKey: ["timeline-9999"],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: ark.account.contacts,
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
});
navigate("/");
}; };
useEffect(() => { useEffect(() => {
@@ -117,7 +88,7 @@ export function OnboardingScreen() {
return ( return (
<div className="relative flex h-full w-full items-center justify-center"> <div className="relative flex h-full w-full items-center justify-center">
<div className="mx-auto flex w-full max-w-md flex-col gap-10"> <div className="mx-auto flex w-full max-w-md flex-col gap-8">
<div className="flex flex-col gap-1 text-center items-center"> <div className="flex flex-col gap-1 text-center items-center">
<h1 className="text-2xl font-semibold"> <h1 className="text-2xl font-semibold">
You&apos;re almost ready to use Lume. You&apos;re almost ready to use Lume.
@@ -194,7 +165,7 @@ export function OnboardingScreen() {
</div> </div>
<div className="relative flex justify-center"> <div className="relative flex justify-center">
<span className="px-2 text-sm font-medium bg-neutral-950 text-neutral-600"> <span className="px-2 text-sm font-medium bg-neutral-950 text-neutral-600">
Not have API ? Don't have an API key?
</span> </span>
</div> </div>
</div> </div>

View File

@@ -1,16 +1,6 @@
import { LoaderIcon } from "@lume/icons"; import { Link } from "react-router-dom";
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
export function WelcomeScreen() { export function WelcomeScreen() {
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
const gotoCreateAccount = () => {
setLoading(true);
navigate("/auth/create");
};
return ( return (
<div className="flex flex-col items-center justify-between w-full h-full"> <div className="flex flex-col items-center justify-between w-full h-full">
<div /> <div />
@@ -29,17 +19,12 @@ export function WelcomeScreen() {
</p> </p>
</div> </div>
<div className="flex flex-col w-full max-w-xs gap-2 mx-auto"> <div className="flex flex-col w-full max-w-xs gap-2 mx-auto">
<button <Link
type="button" to="/auth/create"
onClick={gotoCreateAccount}
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600" className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
> >
{loading ? ( Join Nostr
<LoaderIcon className="size-5 animate-spin" /> </Link>
) : (
"Create New Account"
)}
</button>
<Link <Link
to="/auth/login" to="/auth/login"
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900" className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-neutral-50 rounded-xl bg-neutral-950 hover:bg-neutral-900"
@@ -49,7 +34,7 @@ export function WelcomeScreen() {
</div> </div>
</div> </div>
<div className="flex items-center justify-center h-11"> <div className="flex items-center justify-center h-11">
<p className="text-neutral-800"> <p className="text-neutral-700">
Before joining Nostr, you can take time to learn more about Nostr{" "} Before joining Nostr, you can take time to learn more about Nostr{" "}
<Link <Link
to="https://nostr.com" to="https://nostr.com"

View File

@@ -10,7 +10,7 @@ import { useColumnContext } from "@lume/ark";
import { import {
ArrowLeftIcon, ArrowLeftIcon,
ArrowRightIcon, ArrowRightIcon,
NewColumnIcon, PlusIcon,
PlusSquareIcon, PlusSquareIcon,
} from "@lume/icons"; } from "@lume/icons";
import { IColumn } from "@lume/types"; import { IColumn } from "@lume/types";
@@ -96,15 +96,15 @@ export function HomeScreen() {
content: "", content: "",
}) })
} }
className="size-16 inline-flex items-center justify-center rounded-full bg-blue-500 hover:bg-blue-600 text-white" className="size-16 inline-flex items-center justify-center hover:bg-neutral-100 dark:hover:bg-neutral-900 rounded-2xl"
> >
<NewColumnIcon className="size-7" /> <PlusIcon className="size-6" />
</button> </button>
</div> </div>
</VList> </VList>
<Tooltip.Provider> <Tooltip.Provider>
<div className="absolute bottom-3 right-3"> <div className="absolute bottom-3 right-3">
<div className="flex items-center gap-1 p-1 bg-black/30 dark:bg-white/30 backdrop-blur-xl rounded-xl"> <div className="flex items-center gap-1 p-1 bg-black/50 dark:bg-white/30 backdrop-blur-xl rounded-xl shadow-toolbar">
<Tooltip.Root delayDuration={150}> <Tooltip.Root delayDuration={150}>
<Tooltip.Trigger asChild> <Tooltip.Trigger asChild>
<button <button

View File

@@ -48,7 +48,7 @@ export function BackupSettingScreen() {
<button <button
type="button" type="button"
onClick={() => removePrivkey()} onClick={() => removePrivkey()}
className="mt-2 inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-red-200 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:hover:text-white" className="mt-2 inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-red-200 dark:bg-red-800 px-6 font-medium text-red-500 hover:bg-red-500 hover:text-white focus:outline-none dark:hover:text-white"
> >
Remove private key Remove private key
</button> </button>

View File

@@ -10,6 +10,7 @@ import { NDKKind, NDKUserProfile } from "@nostr-dev-kit/ndk";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { AvatarUpload } from "./components/avatarUpload"; import { AvatarUpload } from "./components/avatarUpload";
import { CoverUpload } from "./components/coverUpload"; import { CoverUpload } from "./components/coverUpload";
@@ -92,6 +93,9 @@ export function ProfileSettingScreen() {
return content; return content;
}); });
// notify
toast.success("You've updated profile successfully.");
// reset state // reset state
setPicture(null); setPicture(null);
setBanner(null); setBanner(null);
@@ -112,7 +116,7 @@ export function ProfileSettingScreen() {
className="h-full w-full rounded-t-xl object-cover" className="h-full w-full rounded-t-xl object-cover"
/> />
) : ( ) : (
<div className="h-full w-full rounded-t-xl bg-neutral-200 dark:bg-neutral-900" /> <div className="h-full w-full rounded-t-xl bg-neutral-200 dark:bg-neutral-800" />
)} )}
<div className="absolute right-4 top-4"> <div className="absolute right-4 top-4">
<CoverUpload setBanner={setBanner} /> <CoverUpload setBanner={setBanner} />
@@ -236,7 +240,7 @@ export function ProfileSettingScreen() {
<button <button
type="submit" type="submit"
disabled={!isValid || loading} disabled={!isValid || loading}
className="mx-auto inline-flex h-10 w-24 transform items-center justify-center gap-1 rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600 focus:outline-none active:translate-y-1 disabled:pointer-events-none disabled:opacity-50" className="inline-flex items-center justify-center w-24 pb-[2px] 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"
> >
{loading ? ( {loading ? (
<LoaderIcon className="size-4 animate-spin" /> <LoaderIcon className="size-4 animate-spin" />

View File

@@ -13,7 +13,7 @@
"@astrojs/check": "^0.4.1", "@astrojs/check": "^0.4.1",
"@astrojs/tailwind": "^5.1.0", "@astrojs/tailwind": "^5.1.0",
"@fontsource/geist-mono": "^5.0.1", "@fontsource/geist-mono": "^5.0.1",
"astro": "^4.2.1", "astro": "^4.2.4",
"astro-seo-meta": "^4.1.0", "astro-seo-meta": "^4.1.0",
"astro-seo-schema": "^4.0.0", "astro-seo-schema": "^4.0.0",
"schema-dts": "^1.1.2", "schema-dts": "^1.1.2",

View File

@@ -8,7 +8,7 @@
"tauri": "tauri" "tauri": "tauri"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.5.2", "@biomejs/biome": "^1.5.3",
"@tauri-apps/cli": "2.0.0-alpha.21", "@tauri-apps/cli": "2.0.0-alpha.21",
"turbo": "^1.11.3" "turbo": "^1.11.3"
}, },

View File

@@ -18,9 +18,9 @@
"@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.17.15", "@tanstack/react-query": "^5.17.19",
"get-urls": "^12.1.0", "get-urls": "^12.1.0",
"jotai": "^2.6.2", "jotai": "^2.6.3",
"linkify-react": "^4.1.3", "linkify-react": "^4.1.3",
"linkifyjs": "^4.1.3", "linkifyjs": "^4.1.3",
"media-chrome": "^2.1.0", "media-chrome": "^2.1.0",
@@ -38,14 +38,14 @@
"string-strip-html": "^13.4.5", "string-strip-html": "^13.4.5",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"use-context-selector": "^1.4.1", "use-context-selector": "^1.4.1",
"virtua": "^0.20.5" "virtua": "^0.21.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^", "@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^", "@lume/types": "workspace:^",
"@types/react": "^18.2.48", "@types/react": "^18.2.48",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.1",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }

View File

@@ -145,7 +145,7 @@ export class Ark {
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST, cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST,
}); });
if (!profile) throw new Error("user not found"); if (!profile) return null;
return profile; return profile;
} catch { } catch {
throw new Error("user not found"); throw new Error("user not found");

View File

@@ -1,5 +1,5 @@
import { import {
HorizontalDotsIcon, ChevronDownIcon,
MoveLeftIcon, MoveLeftIcon,
MoveRightIcon, MoveRightIcon,
RefreshIcon, RefreshIcon,
@@ -43,32 +43,26 @@ export function ColumnHeader({
}; };
return ( return (
<div className="flex items-center justify-between w-full px-3 border-b h-11 shrink-0 border-neutral-100 dark:border-neutral-900">
<div className="inline-flex items-center gap-4">
<div className="inline-flex items-center flex-1 gap-2 text-neutral-800 dark:text-neutral-200">
{icon ? icon : <ThreadIcon className="size-4" />}
<div className="text-sm font-medium">{title}</div>
</div>
</div>
<div>
<DropdownMenu.Root> <DropdownMenu.Root>
<div className="flex items-center justify-center gap-2 px-3 w-full border-b h-11 shrink-0 border-neutral-100 dark:border-neutral-900">
<DropdownMenu.Trigger asChild> <DropdownMenu.Trigger asChild>
<button <div className="inline-flex items-center gap-1.5">
type="button" <div className="text-[13px] font-medium">{title}</div>
className="inline-flex items-center justify-center w-6 h-6" <ChevronDownIcon className="size-5" />
> </div>
<HorizontalDotsIcon className="size-4" />
</button>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-black/70 dark:bg-white/10 backdrop-blur-xl focus:outline-none"> <DropdownMenu.Content
sideOffset={5}
className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-white/50 dark:bg-black/50 ring-1 ring-black/10 dark:ring-white/10 backdrop-blur-2xl focus:outline-none"
>
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<button <button
type="button" type="button"
onClick={refresh} onClick={refresh}
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
<RefreshIcon className="size-5" /> <RefreshIcon className="size-4" />
Refresh Refresh
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
@@ -76,7 +70,7 @@ export function ColumnHeader({
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<InterestModal <InterestModal
queryKey={queryKey} queryKey={queryKey}
className="text-sm font-medium text-white/50 hover:bg-black/10 hover:text-white dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
/> />
</DropdownMenu.Item> </DropdownMenu.Item>
) : null} ) : null}
@@ -84,9 +78,9 @@ export function ColumnHeader({
<button <button
type="button" type="button"
onClick={moveLeft} onClick={moveLeft}
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
<MoveLeftIcon className="size-5" /> <MoveLeftIcon className="size-4" />
Move left Move left
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
@@ -94,27 +88,26 @@ export function ColumnHeader({
<button <button
type="button" type="button"
onClick={moveRight} onClick={moveRight}
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
<MoveRightIcon className="size-5" /> <MoveRightIcon className="size-4" />
Move right Move right
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Separator className="h-px my-1 bg-white/10 dark:bg-black/10" /> <DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<button <button
type="button" type="button"
onClick={deleteWidget} onClick={deleteWidget}
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-red-300 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none" className="inline-flex items-center gap-3 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
> >
<TrashIcon className="size-5" /> <TrashIcon className="size-4" />
Delete Delete
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Portal> </DropdownMenu.Portal>
</div>
</DropdownMenu.Root> </DropdownMenu.Root>
</div>
</div>
); );
} }

View File

@@ -40,7 +40,7 @@ export function InterestModal({
); );
if (save) { if (save) {
storage.interests.hashtags = hashtags; storage.interests = { hashtags, users: [], words: [] };
await queryClient.refetchQueries({ queryKey }); await queryClient.refetchQueries({ queryKey });
} }
@@ -56,7 +56,7 @@ export function InterestModal({
<Dialog.Root open={open} onOpenChange={setOpen}> <Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger <Dialog.Trigger
className={cn( className={cn(
"inline-flex items-center gap-2 px-3 rounded-lg h-9 focus:outline-none", "inline-flex items-center gap-3 px-3 rounded-lg h-9 focus:outline-none",
className, className,
)} )}
> >
@@ -64,7 +64,7 @@ export function InterestModal({
children children
) : ( ) : (
<> <>
<EditInterestIcon className="size-5" /> <EditInterestIcon className="size-4" />
Edit interest Edit interest
</> </>
)} )}
@@ -86,11 +86,8 @@ export function InterestModal({
<div className="w-full flex-1 min-h-0 flex flex-col justify-between"> <div className="w-full flex-1 min-h-0 flex flex-col justify-between">
<div className="flex-1 min-h-0 overflow-y-auto px-8 py-8"> <div className="flex-1 min-h-0 overflow-y-auto px-8 py-8">
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
{TOPICS.map((topic, index) => ( {TOPICS.map((topic) => (
<div <div key={topic.title} className="flex flex-col gap-4">
key={topic.title + index}
className="flex flex-col gap-4"
>
<div className="w-full flex items-center justify-between"> <div className="w-full flex items-center justify-between">
<div className="inline-flex items-center gap-2.5"> <div className="inline-flex items-center gap-2.5">
<img <img
@@ -113,6 +110,7 @@ export function InterestModal({
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
{topic.content.map((hashtag) => ( {topic.content.map((hashtag) => (
<button <button
key={hashtag}
type="button" type="button"
onClick={() => toggleHashtag(hashtag)} onClick={() => toggleHashtag(hashtag)}
className={cn( className={cn(

View File

@@ -1,4 +1,4 @@
import { ChevronUpIcon } from "@lume/icons"; import { ArrowUpIcon } from "@lume/icons";
import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useArk } from "../../hooks/useArk"; import { useArk } from "../../hooks/useArk";
@@ -34,14 +34,14 @@ export function ColumnLiveWidget({
if (!events.length) return null; if (!events.length) return null;
return ( return (
<div className="absolute left-0 z-50 flex items-center justify-center w-full top-11 h-11"> <div className="absolute left-0 z-40 flex items-center justify-center w-full top-12 h-11">
<button <button
type="button" type="button"
onClick={update} onClick={update}
className="inline-flex items-center justify-center h-8 gap-1 pl-2 pr-2.5 text-sm font-semibold rounded-full w-max bg-neutral-950 dark:bg-neutral-50 hover:bg-neutral-900 dark:hover:bg-neutral-100 text-neutral-50 dark:text-neutral-950" className="inline-flex items-center justify-center h-9 gap-1 pl-4 pr-3 text-sm font-semibold rounded-full w-max bg-neutral-950 dark:bg-neutral-50 hover:bg-neutral-900 dark:hover:bg-neutral-100 text-neutral-50 dark:text-neutral-950"
> >
<ChevronUpIcon className="w-4 h-4" />
{events.length} {events.length === 1 ? "new note" : "new notes"} {events.length} {events.length === 1 ? "new note" : "new notes"}
<ArrowUpIcon className="size-4" />
</button> </button>
</div> </div>
); );

View File

@@ -55,7 +55,10 @@ export function ColumnProvider({ children }: { children: ReactNode }) {
column.title, column.title,
column.content, column.content,
); );
if (result) setColumns((prev) => [...prev, result]); if (result) {
setColumns((prev) => [...prev, result]);
vlistRef?.current.scrollToIndex(columns.length);
}
}, []); }, []);
const removeColumn = useCallback(async (id: number) => { const removeColumn = useCallback(async (id: number) => {

View File

@@ -1,4 +1,4 @@
import { ChatsIcon } from "@lume/icons"; import { ReplyIcon } from "@lume/icons";
import * as Tooltip from "@radix-ui/react-tooltip"; import * as Tooltip from "@radix-ui/react-tooltip";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useNoteContext } from "../provider"; import { useNoteContext } from "../provider";
@@ -16,12 +16,12 @@ export function NoteReply() {
onClick={() => navigate(`/events/${event.id}`)} onClick={() => navigate(`/events/${event.id}`)}
className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400" className="inline-flex items-center justify-center group h-7 w-7 text-neutral-600 dark:text-neutral-400"
> >
<ChatsIcon className="size-5 group-hover:text-blue-500" /> <ReplyIcon className="size-5 group-hover:text-blue-500" />
</button> </button>
</Tooltip.Trigger> </Tooltip.Trigger>
<Tooltip.Portal> <Tooltip.Portal>
<Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade"> <Tooltip.Content className="inline-flex h-7 select-none text-neutral-50 dark:text-neutral-950 items-center justify-center rounded-md bg-neutral-950 dark:bg-neutral-50 px-3.5 text-sm will-change-[transform,opacity] data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade">
Quick reply View thread
<Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" /> <Tooltip.Arrow className="fill-neutral-950 dark:fill-neutral-50" />
</Tooltip.Content> </Tooltip.Content>
</Tooltip.Portal> </Tooltip.Portal>

View File

@@ -88,12 +88,12 @@ export function NoteRepost() {
</Tooltip.Root> </Tooltip.Root>
</Tooltip.Provider> </Tooltip.Provider>
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-black/70 dark:bg-white/10 backdrop-blur-xl focus:outline-none"> <DropdownMenu.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-white/50 dark:bg-black/50 ring-1 ring-black/10 dark:ring-white/10 backdrop-blur-2xl focus:outline-none">
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<button <button
type="button" type="button"
onClick={repost} onClick={repost}
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
<RepostIcon className="size-4" /> <RepostIcon className="size-4" />
Repost Repost
@@ -103,7 +103,7 @@ export function NoteRepost() {
<button <button
type="button" type="button"
onClick={quote} onClick={quote}
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
<ReplyIcon className="size-4" /> <ReplyIcon className="size-4" />
Quote Quote

View File

@@ -131,10 +131,18 @@ export function NoteZap() {
</Tooltip.Root> </Tooltip.Root>
</Tooltip.Provider> </Tooltip.Provider>
<Dialog.Portal> <Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 z-50 bg-black/10 backdrop-blur-sm dark:bg-white/10" /> <Dialog.Overlay className="fixed inset-0 z-50 bg-black/20 backdrop-blur-sm dark:bg-white/20" />
<Dialog.Content className="fixed inset-0 z-50 flex items-center justify-center min-h-full"> <Dialog.Content className="fixed inset-0 z-50 flex items-center justify-center min-h-full">
<Dialog.Close className="absolute top-5 right-5 z-50">
<div className="flex flex-col gap-1.5">
<div className="inline-flex items-center justify-center size-10 rounded-lg bg-white dark:bg-black">
<CancelIcon className="size-5" />
</div>
<span className="text-sm font-medium">Esc</span>
</div>
</Dialog.Close>
<div className="relative w-full max-w-xl bg-white h-min rounded-xl dark:bg-black"> <div className="relative w-full max-w-xl bg-white h-min rounded-xl dark:bg-black">
<div className="inline-flex items-center justify-between w-full px-5 py-3 shrink-0"> <div className="inline-flex items-center justify-center w-full px-5 py-3 shrink-0">
<div className="w-6" /> <div className="w-6" />
<Dialog.Title className="font-semibold text-center"> <Dialog.Title className="font-semibold text-center">
Send zap to{" "} Send zap to{" "}
@@ -142,13 +150,10 @@ export function NoteZap() {
user?.displayName || user?.displayName ||
displayNpub(event.pubkey, 16)} displayNpub(event.pubkey, 16)}
</Dialog.Title> </Dialog.Title>
<Dialog.Close className="inline-flex items-center justify-center w-6 h-6 rounded-md bg-neutral-100 dark:bg-neutral-900">
<CancelIcon className="w-4 h-4" />
</Dialog.Close>
</div> </div>
{!invoice ? ( {!invoice ? (
<div className="px-5 pb-5 overflow-x-hidden overflow-y-auto"> <div className="px-5 pb-5 overflow-x-hidden overflow-y-auto">
<div className="relative flex flex-col h-40"> <div className="relative flex flex-col h-36">
<div className="inline-flex items-center justify-center flex-1 h-full gap-1"> <div className="inline-flex items-center justify-center flex-1 h-full gap-1">
<CurrencyInput <CurrencyInput
placeholder="0" placeholder="0"
@@ -213,13 +218,13 @@ export function NoteZap() {
autoCorrect="off" autoCorrect="off"
autoCapitalize="off" autoCapitalize="off"
placeholder="Enter message (optional)" 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" 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-950 dark:text-neutral-400"
/> />
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<button <button
type="button" type="button"
onClick={() => createZapRequest()} 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" className="inline-flex items-center justify-center w-full pb-[2px] 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"
> >
{isCompleted {isCompleted
? "Zapped" ? "Zapped"

View File

@@ -14,18 +14,18 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
return ( return (
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger className="text-blue-500 break-words hover:text-blue-600"> <DropdownMenu.Trigger className="text-start text-blue-500 break-words hover:text-blue-600">
{isLoading {isLoading
? "@anon" ? "@anon"
: isError : isError
? pubkey ? pubkey
: `@${user?.name || user?.displayName || user?.username || "anon"}`} : `@${user?.name || user?.displayName || user?.username || "anon"}`}
</DropdownMenu.Trigger> </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.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-white/50 dark:bg-black/50 ring-1 ring-black/10 dark:ring-white/10 backdrop-blur-2xl focus:outline-none">
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<Link <Link
to={`/users/${cleanPubkey}`} to={`/users/${cleanPubkey}`}
className="inline-flex items-center h-10 px-4 text-sm text-white hover:bg-neutral-900 focus:outline-none" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
View profile View profile
</Link> </Link>
@@ -40,7 +40,7 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
content: cleanPubkey, content: cleanPubkey,
}) })
} }
className="inline-flex items-center h-10 px-4 text-sm text-white hover:bg-neutral-900 focus:outline-none" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
Pin Pin
</button> </button>

View File

@@ -1,15 +1,19 @@
import { HorizontalDotsIcon } from "@lume/icons"; import { HorizontalDotsIcon } from "@lume/icons";
import { COL_TYPES } from "@lume/utils";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { writeText } from "@tauri-apps/plugin-clipboard-manager"; import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { type EventPointer } from "nostr-tools/lib/types/nip19"; import { type EventPointer } from "nostr-tools/lib/types/nip19";
import { useState } from "react"; import { useState } from "react";
import { Link } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { toast } from "sonner"; import { toast } from "sonner";
import { useColumnContext } from "../column/provider";
import { useNoteContext } from "./provider"; import { useNoteContext } from "./provider";
export function NoteMenu() { export function NoteMenu() {
const event = useNoteContext(); const event = useNoteContext();
const navigate = useNavigate();
const { addColumn } = useColumnContext();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const copyID = async () => { const copyID = async () => {
@@ -50,18 +54,27 @@ export function NoteMenu() {
<DropdownMenu.Trigger asChild> <DropdownMenu.Trigger asChild>
<button <button
type="button" type="button"
className="inline-flex items-center justify-center w-6 h-6" className="inline-flex items-center justify-center size-6"
> >
<HorizontalDotsIcon className="w-4 h-4 text-neutral-800 hover:text-blue-500 dark:text-neutral-200" /> <HorizontalDotsIcon className="size-4 hover:text-blue-500 dark:text-neutral-200" />
</button> </button>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-black/70 dark:bg-white/10 backdrop-blur-xl focus:outline-none"> <DropdownMenu.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-white/50 dark:bg-black/50 ring-1 ring-black/10 dark:ring-white/10 backdrop-blur-2xl focus:outline-none">
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<button <button
type="button" type="button"
onClick={() => copyLink()} onClick={() => copyLink()}
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
View thread
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => navigate(`/events/${event.id}`)}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
Copy shareable link Copy shareable link
</button> </button>
@@ -70,34 +83,49 @@ export function NoteMenu() {
<button <button
type="button" type="button"
onClick={() => copyID()} onClick={() => copyID()}
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
Copy ID Copy note ID
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<button <button
type="button" type="button"
onClick={() => copyNpub()} onClick={() => copyNpub()}
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
Copy npub Copy author ID
</button> </button>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<Link <Link
to={`/users/${event.pubkey}`} to={`/users/${event.pubkey}`}
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
View profile View author
</Link> </Link>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Separator className="h-px my-1 bg-white/10 dark:bg-black/10" /> <DropdownMenu.Item asChild>
<button
type="button"
onClick={() =>
addColumn({
kind: COL_TYPES.user,
title: "User",
content: event.pubkey,
})
}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
Pin author
</button>
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<button <button
type="button" type="button"
onClick={() => copyRaw()} onClick={() => copyRaw()}
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
Copy raw event Copy raw event
</button> </button>
@@ -106,7 +134,7 @@ export function NoteMenu() {
<button <button
type="button" type="button"
onClick={muteUser} onClick={muteUser}
className="inline-flex items-center gap-2 px-3 text-sm font-medium text-red-300 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none" className="inline-flex items-center gap-3 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
> >
Mute Mute
</button> </button>

View File

@@ -48,12 +48,12 @@ export function ImagePreview({ url }: { url: string }) {
<button <button
type="button" type="button"
onClick={(e) => downloadImage(e)} onClick={(e) => downloadImage(e)}
className="absolute z-10 items-center justify-center hidden size-8 bg-white/10 text-white backdrop-blur-xl rounded-lg right-2 top-2 group-hover:inline-flex hover:bg-blue-500" className="absolute z-10 items-center justify-center hidden size-10 bg-white/10 text-black/70 backdrop-blur-xl rounded-lg right-2 top-2 group-hover:inline-flex hover:bg-blue-500 hover:text-white"
> >
{downloaded ? ( {downloaded ? (
<CheckCircleIcon className="size-4" /> <CheckCircleIcon className="size-5" />
) : ( ) : (
<DownloadIcon className="size-4" /> <DownloadIcon className="size-5" />
)} )}
</button> </button>
</div> </div>

View File

@@ -8,7 +8,7 @@ export function ChildReply({
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root className="py-2"> <Note.Root className="py-2">
<div className="flex items-center justify-between h-14"> <div className="flex items-center justify-between h-14">
<Note.User className="flex-1" /> <Note.User className="flex-1 pr-2" />
<Note.Menu /> <Note.Menu />
</div> </div>
<Note.Content /> <Note.Content />

View File

@@ -18,7 +18,7 @@ export function Reply({
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root className="pt-2"> <Note.Root className="pt-2">
<div className="flex items-center justify-between h-14"> <div className="flex items-center justify-between h-14">
<Note.User className="flex-1 pr-1" /> <Note.User className="flex-1 pr-2" />
<Note.Menu /> <Note.Menu />
</div> </div>
<Note.Content /> <Note.Content />

View File

@@ -93,7 +93,7 @@ export function RepostNote({
<Note.Provider event={repostEvent}> <Note.Provider event={repostEvent}>
<div className="relative flex flex-col gap-2 px-3"> <div className="relative flex flex-col gap-2 px-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Note.User className="flex-1 pr-1" /> <Note.User className="flex-1 pr-2" />
<Note.Menu /> <Note.Menu />
</div> </div>
<Note.Content /> <Note.Content />

View File

@@ -15,7 +15,7 @@ export function TextNote({
)} )}
> >
<div className="flex items-center justify-between px-3 h-14"> <div className="flex items-center justify-between px-3 h-14">
<Note.User className="flex-1 pr-1" /> <Note.User className="flex-1 pr-2" />
<Note.Menu /> <Note.Menu />
</div> </div>
<Note.Thread className="mb-2" /> <Note.Thread className="mb-2" />

View File

@@ -87,31 +87,18 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
async function initNDK() { async function initNDK() {
const explicitRelayUrls = normalizeRelayUrlSet([ const explicitRelayUrls = normalizeRelayUrlSet([
"wss://nostr.mutinywallet.com/",
"wss://bostr.nokotaro.com/", "wss://bostr.nokotaro.com/",
]); ]);
// #TODO: user should config outbox relays
const outboxRelayUrls = normalizeRelayUrlSet(["wss://purplepag.es/"]);
// #TODO: user should config blacklist relays
// Skip connect depot tunnel url
const blacklistRelayUrls = normalizeRelayUrlSet(
storage.settings.tunnelUrl.length
? [storage.settings.tunnelUrl, "wss://brb.io/"]
: ["wss://brb.io/"],
);
const tauriCache = new NDKCacheAdapterTauri(storage); const tauriCache = new NDKCacheAdapterTauri(storage);
const ndk = new NDK({ const ndk = new NDK({
cacheAdapter: tauriCache, cacheAdapter: tauriCache,
explicitRelayUrls, explicitRelayUrls,
outboxRelayUrls,
blacklistRelayUrls,
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: '',
}); });
// use tauri fetch // use tauri fetch

View File

@@ -111,3 +111,4 @@ export * from "./src/foryou";
export * from "./src/editInterest"; export * from "./src/editInterest";
export * from "./src/newColumn"; export * from "./src/newColumn";
export * from "./src/searchFilled"; export * from "./src/searchFilled";
export * from "./src/arrowUp";

View File

@@ -0,0 +1,24 @@
import { SVGProps } from "react";
export function ArrowUpIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
{...props}
>
<path
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 9.83a30.23 30.23 0 015.406-5.62A.949.949 0 0112 4m6 5.83a30.233 30.233 0 00-5.406-5.62A.949.949 0 0012 4m0 0v16"
/>
</svg>
);
}

View File

@@ -17,7 +17,7 @@ export function ChevronDownIcon(
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="2" strokeWidth="2"
d="M8 10.14a20.36 20.36 0 003.702 3.893c.175.141.42.141.596 0A20.361 20.361 0 0016 10.14" d="M6 9a30.618 30.618 0 005.49 5.817c.3.244.72.244 1.02 0A30.617 30.617 0 0018 9"
/> />
</svg> </svg>
); );

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import { SVGProps } from "react";
export function ChevronUpIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function ChevronUpIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -14,9 +16,9 @@ export function ChevronUpIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGEl
stroke="currentColor" stroke="currentColor"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="1.5" strokeWidth="2"
d="M8 14l3.646-3.646a.5.5 0 01.708 0L16 14" d="M6 15a30.617 30.617 0 015.49-5.817.803.803 0 011.02 0A30.616 30.616 0 0118 15"
></path> />
</svg> </svg>
); );
} }

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import { SVGProps } from "react";
export function DownloadIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function DownloadIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -14,9 +16,9 @@ export function DownloadIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGEle
stroke="currentColor" stroke="currentColor"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="1.5" strokeWidth="2"
d="M20.25 14.75v4.5a1 1 0 01-1 1H4.75a1 1 0 01-1-1v-4.5M12 15V3.75M12 15l-3.5-3.5M12 15l3.5-3.5" d="M3 15a5 5 0 005 5h8a5 5 0 005-5M9 12.188a15 15 0 002.556 2.655c.13.104.287.157.444.157m3-2.812a14.998 14.998 0 01-2.556 2.655A.704.704 0 0112 15m0 0V4"
></path> />
</svg> </svg>
); );
} }

View File

@@ -17,7 +17,7 @@ export function LogoutIcon(
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="2" strokeWidth="2"
d="M18.189 9a15 15 0 012.654 2.556c.105.13.157.287.157.444m-2.811 3a14.998 14.998 0 002.654-2.556A.704.704 0 0021 12m0 0H8m5-7.472A6 6 0 003 9v6a6 6 0 0010 4.472" d="M5.812 9a15.001 15.001 0 00-2.655 2.556A.703.703 0 003 12m2.812 3a15 15 0 01-2.655-2.556A.703.703 0 013 12m0 0h13m-5-7.472A6 6 0 0121 9v6a6 6 0 01-10 4.472"
/> />
</svg> </svg>
); );

View File

@@ -6,13 +6,15 @@ export function RelayIcon(props: JSX.IntrinsicElements["svg"]) {
height="24" height="24"
fill="none" fill="none"
viewBox="0 0 25 24" viewBox="0 0 25 24"
{...props}
>
<path
stroke="currentColor" stroke="currentColor"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="2" strokeWidth="2"
{...props} d="M21.5 12a9.002 9.002 0 01-4.682 7.897 9 9 0 01-5.59 1.013c-1.203-.17-1.805-.255-1.964-.267-.257-.02-.165-.016-.423-.014-.159 0-.34.014-.702.04l-2.153.153c-.857.062-1.286.092-1.607-.06a1.348 1.348 0 01-.641-.64c-.152-.32-.122-.75-.06-1.608l.153-2.153c.026-.362.04-.542.04-.702.002-.258.006-.166-.014-.423-.012-.159-.098-.76-.268-1.964A9 9 0 1121.5 12z"
> />
<path d="M12.5 20.875V17m0 3.875c-1.75 0-3.5.375-5 1.125m5-1.125c1.75 0 3.5.375 5 1.125m-5-5h7.6c.84 0 1.26 0 1.581-.163a1.5 1.5 0 00.655-.656c.164-.32.164-.74.164-1.581V4.4c0-.84 0-1.26-.163-1.581a1.5 1.5 0 00-.656-.656C21.361 2 20.941 2 20.1 2H4.9c-.84 0-1.26 0-1.581.163a1.5 1.5 0 00-.656.656c-.163.32-.163.74-.163 1.581v10.2c0 .84 0 1.26.163 1.581a1.5 1.5 0 00.656.655c.32.164.74.164 1.581.164h7.6z" />
</svg> </svg>
); );
} }

View File

@@ -1,18 +1,20 @@
export function ReplyIcon(props: JSX.IntrinsicElements["svg"]) { export function ReplyIcon(props: JSX.IntrinsicElements["svg"]) {
return ( return (
<svg <svg
{...props}
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="25" width="25"
height="24" height="24"
fill="none" fill="none"
viewBox="0 0 25 24" viewBox="0 0 25 24"
{...props}
>
<path
stroke="currentColor" stroke="currentColor"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="2" strokeWidth="2"
> d="M21.5 12a9.002 9.002 0 01-4.682 7.897 9 9 0 01-5.59 1.013c-1.203-.17-1.805-.255-1.964-.267-.257-.02-.165-.016-.423-.014-.159 0-.34.014-.702.04l-2.153.153c-.857.062-1.286.092-1.607-.06a1.348 1.348 0 01-.641-.64c-.152-.32-.122-.75-.06-1.608l.153-2.153c.026-.362.04-.542.04-.702.002-.258.006-.166-.014-.423-.012-.159-.098-.76-.268-1.964A9 9 0 1121.5 12z"
<path d="M9.5 12h3m0 0h3m-3 0V9m0 3v3m9-3a9 9 0 01-10.272 8.91c-1.203-.17-1.805-.255-1.964-.267-.257-.02-.165-.016-.423-.014-.159 0-.34.014-.702.04l-2.153.153c-.857.062-1.286.092-1.607-.06a1.35 1.35 0 01-.641-.641c-.152-.32-.122-.75-.06-1.607l.153-2.153c.026-.362.04-.543.04-.702.002-.258.006-.166-.014-.423-.012-.159-.098-.76-.268-1.964A9 9 0 1121.5 12z" /> />
</svg> </svg>
); );
} }

View File

@@ -1,6 +1,8 @@
import { SVGProps } from 'react'; import { SVGProps } from "react";
export function UserIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) { export function UserIcon(
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
) {
return ( return (
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -12,10 +14,11 @@ export function UserIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement
> >
<path <path
stroke="currentColor" stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth="1.5" strokeWidth="2"
d="M5.857 18.916C7.171 16.996 9.332 15.75 12 15.75c2.668 0 4.83 1.247 6.143 3.166m-12.286 0A9.215 9.215 0 0012 21.25c2.358 0 4.51-.882 6.143-2.334m-12.286 0a9.25 9.25 0 1112.286 0M15.25 10a3.25 3.25 0 11-6.5 0 3.25 3.25 0 016.5 0z" d="M18.995 19.147C18.893 17.393 17.367 16 15.5 16h-7c-1.867 0-3.393 1.393-3.495 3.147m13.99 0A9.97 9.97 0 0022 12c0-5.523-4.477-10-10-10S2 6.477 2 12a9.97 9.97 0 003.005 7.147m13.99 0A9.967 9.967 0 0112 22a9.967 9.967 0 01-6.995-2.853M15 10a3 3 0 11-6 0 3 3 0 016 0z"
></path> />
</svg> </svg>
); );
} }

View File

@@ -1,26 +0,0 @@
{
"name": "@columns/activity",
"version": "0.0.0",
"private": true,
"main": "./src/index.tsx",
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@nostr-dev-kit/ndk": "^2.3.3",
"@tanstack/react-query": "^5.17.15",
"react": "^18.2.0",
"react-router-dom": "^6.21.3",
"sonner": "^1.3.1",
"virtua": "^0.20.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.48",
"tailwind": "^4.0.0",
"typescript": "^5.3.3"
}
}

View File

@@ -1,8 +0,0 @@
import sharedConfig from "@lume/tailwindcss";
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
presets: [sharedConfig],
};
export default config;

View File

@@ -1,8 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -9,11 +9,11 @@
"@lume/ui": "workspace:^", "@lume/ui": "workspace:^",
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@nostr-dev-kit/ndk": "^2.3.3", "@nostr-dev-kit/ndk": "^2.3.3",
"@tanstack/react-query": "^5.17.15", "@tanstack/react-query": "^5.17.19",
"react": "^18.2.0", "react": "^18.2.0",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
"sonner": "^1.3.1", "sonner": "^1.3.1",
"virtua": "^0.20.5" "virtua": "^0.21.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",

View File

@@ -9,11 +9,11 @@
"@lume/ui": "workspace:^", "@lume/ui": "workspace:^",
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@nostr-dev-kit/ndk": "^2.3.3", "@nostr-dev-kit/ndk": "^2.3.3",
"@tanstack/react-query": "^5.17.15", "@tanstack/react-query": "^5.17.19",
"react": "^18.2.0", "react": "^18.2.0",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
"sonner": "^1.3.1", "sonner": "^1.3.1",
"virtua": "^0.20.5" "virtua": "^0.21.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",

View File

@@ -14,19 +14,36 @@ export function Default({ column }: { column: IColumn }) {
icon={<ColumnIcon className="size-4" />} icon={<ColumnIcon className="size-4" />}
/> />
<div className="h-full px-3 mt-3 flex flex-col gap-3 overflow-y-auto scrollbar-none"> <div className="h-full px-3 mt-3 flex flex-col gap-3 overflow-y-auto scrollbar-none">
<div className="flex flex-col rounded-xl overflow-hidden"> <div className="h-11 flex items-center gap-5">
<div className="h-[100px] w-full"> <button
type="button"
className="h-9 w-max px-3 text-sm font-semibold inline-flex items-center justify-center bg-neutral-100 dark:bg-neutral-900 rounded-lg"
>
Official
</button>
<button
type="button"
disabled
className="h-9 w-max px-3 text-sm inline-flex items-center justify-center rounded-lg disabled:opacity-50"
>
Community (Coming Soon)
</button>
</div>
<div className="flex flex-col rounded-xl overflow-hidden bg-neutral-50 dark:bg-neutral-950 ring-1 ring-neutral-100 dark:ring-neutral-900">
<div className="h-[100px] w-full px-3 pt-3">
<img <img
src="/columns/group.jpg" src="/columns/group.jpg"
srcSet="/columns/group@2x.jpg 2x" srcSet="/columns/group@2x.jpg 2x"
alt="group" alt="group"
className="w-full h-auto object-cover" loading="lazy"
decoding="async"
className="w-full h-auto object-cover rounded-lg"
/> />
</div> </div>
<div className="h-16 shrink-0 px-3 flex items-center justify-between bg-neutral-50 dark:bg-neutral-950"> <div className="h-16 shrink-0 px-3 flex items-center justify-between">
<div> <div>
<h1 className="font-semibold">Group Feeds</h1> <h1 className="font-semibold">Group Feeds</h1>
<p className="max-w-[18rem] truncate text-sm text-neutral-500 dark:text-neutral-600"> <p className="max-w-[18rem] truncate text-sm text-neutral-600 dark:text-neutral-500">
Collective of people you're interested in. Collective of people you're interested in.
</p> </p>
</div> </div>
@@ -41,19 +58,21 @@ export function Default({ column }: { column: IColumn }) {
</button> </button>
</div> </div>
</div> </div>
<div className="flex flex-col rounded-xl overflow-hidden"> <div className="flex flex-col rounded-xl overflow-hidden bg-neutral-50 dark:bg-neutral-950 ring-1 ring-neutral-100 dark:ring-neutral-900">
<div className="h-[100px] w-full"> <div className="h-[100px] w-full px-3 pt-3">
<img <img
src="/columns/antenas.jpg" src="/columns/antenas.jpg"
srcSet="/columns/antenas@2x.jpg 2x" srcSet="/columns/antenas@2x.jpg 2x"
alt="antenas" alt="antenas"
className="w-full h-auto object-cover" loading="lazy"
decoding="async"
className="w-full h-auto object-cover rounded-lg"
/> />
</div> </div>
<div className="h-16 shrink-0 px-3 flex items-center justify-between bg-neutral-50 dark:bg-neutral-950"> <div className="h-16 shrink-0 px-3 flex items-center justify-between">
<div> <div>
<h1 className="font-semibold">Antenas</h1> <h1 className="font-semibold">Antenas</h1>
<p className="max-w-[18rem] truncate text-sm text-neutral-500 dark:text-neutral-600"> <p className="max-w-[18rem] truncate text-sm text-neutral-600 dark:text-neutral-500">
Keep track to specific content. Keep track to specific content.
</p> </p>
</div> </div>

View File

@@ -10,11 +10,11 @@
"@lume/ui": "workspace:^", "@lume/ui": "workspace:^",
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@nostr-dev-kit/ndk": "^2.3.3", "@nostr-dev-kit/ndk": "^2.3.3",
"@tanstack/react-query": "^5.17.15", "@tanstack/react-query": "^5.17.19",
"react": "^18.2.0", "react": "^18.2.0",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
"sonner": "^1.3.1", "sonner": "^1.3.1",
"virtua": "^0.20.5" "virtua": "^0.21.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",

View File

@@ -9,11 +9,11 @@
"@lume/ui": "workspace:^", "@lume/ui": "workspace:^",
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@nostr-dev-kit/ndk": "^2.3.3", "@nostr-dev-kit/ndk": "^2.3.3",
"@tanstack/react-query": "^5.17.15", "@tanstack/react-query": "^5.17.19",
"react": "^18.2.0", "react": "^18.2.0",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
"sonner": "^1.3.1", "sonner": "^1.3.1",
"virtua": "^0.20.5" "virtua": "^0.21.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",

View File

@@ -9,11 +9,11 @@
"@lume/ui": "workspace:^", "@lume/ui": "workspace:^",
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@nostr-dev-kit/ndk": "^2.3.3", "@nostr-dev-kit/ndk": "^2.3.3",
"@tanstack/react-query": "^5.17.15", "@tanstack/react-query": "^5.17.19",
"react": "^18.2.0", "react": "^18.2.0",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
"sonner": "^1.3.1", "sonner": "^1.3.1",
"virtua": "^0.20.5" "virtua": "^0.21.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",

View File

@@ -9,11 +9,11 @@
"@lume/ui": "workspace:^", "@lume/ui": "workspace:^",
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@nostr-dev-kit/ndk": "^2.3.3", "@nostr-dev-kit/ndk": "^2.3.3",
"@tanstack/react-query": "^5.17.15", "@tanstack/react-query": "^5.17.19",
"react": "^18.2.0", "react": "^18.2.0",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
"sonner": "^1.3.1", "sonner": "^1.3.1",
"virtua": "^0.20.5" "virtua": "^0.21.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",

View File

@@ -9,11 +9,11 @@
"@lume/ui": "workspace:^", "@lume/ui": "workspace:^",
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@nostr-dev-kit/ndk": "^2.3.3", "@nostr-dev-kit/ndk": "^2.3.3",
"@tanstack/react-query": "^5.17.15", "@tanstack/react-query": "^5.17.19",
"react": "^18.2.0", "react": "^18.2.0",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
"sonner": "^1.3.1", "sonner": "^1.3.1",
"virtua": "^0.20.5" "virtua": "^0.21.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",

View File

@@ -30,7 +30,7 @@ export function Timeline({ column }: { column: IColumn }) {
id={column.id} id={column.id}
queryKey={[colKey]} queryKey={[colKey]}
title="Timeline" title="Timeline"
icon={<TimelineIcon className="size-4" />} icon={<TimelineIcon className="size-5" />}
/> />
{ark.account.contacts.length ? ( {ark.account.contacts.length ? (
<Column.Live <Column.Live

View File

@@ -9,11 +9,11 @@
"@lume/ui": "workspace:^", "@lume/ui": "workspace:^",
"@lume/utils": "workspace:^", "@lume/utils": "workspace:^",
"@nostr-dev-kit/ndk": "^2.3.3", "@nostr-dev-kit/ndk": "^2.3.3",
"@tanstack/react-query": "^5.17.15", "@tanstack/react-query": "^5.17.19",
"react": "^18.2.0", "react": "^18.2.0",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
"sonner": "^1.3.1", "sonner": "^1.3.1",
"virtua": "^0.20.5" "virtua": "^0.21.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",

View File

@@ -17,9 +17,9 @@
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@tanstack/react-query": "^5.17.15", "@tanstack/react-query": "^5.17.19",
"framer-motion": "^10.18.0", "framer-motion": "^10.18.0",
"jotai": "^2.6.2", "jotai": "^2.6.3",
"minidenticons": "^4.2.0", "minidenticons": "^4.2.0",
"nostr-tools": "~1.17.0", "nostr-tools": "~1.17.0",
"react": "^18.2.0", "react": "^18.2.0",
@@ -28,18 +28,18 @@
"react-hotkeys-hook": "^4.4.4", "react-hotkeys-hook": "^4.4.4",
"react-router-dom": "^6.21.3", "react-router-dom": "^6.21.3",
"slate": "^0.101.5", "slate": "^0.101.5",
"slate-react": "^0.101.5", "slate-react": "^0.101.6",
"sonner": "^1.3.1", "sonner": "^1.3.1",
"uqr": "^0.1.2", "uqr": "^0.1.2",
"use-debounce": "^10.0.0", "use-debounce": "^10.0.0",
"virtua": "^0.20.5" "virtua": "^0.21.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^", "@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^", "@lume/types": "workspace:^",
"@types/react": "^18.2.48", "@types/react": "^18.2.48",
"tailwind-merge": "^2.2.0", "tailwind-merge": "^2.2.1",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }

View File

@@ -1,5 +1,5 @@
import { useArk, useProfile } from "@lume/ark"; import { useArk, useProfile } from "@lume/ark";
import { SettingsIcon } from "@lume/icons"; import { SettingsIcon, UserIcon } from "@lume/icons";
import { cn, useNetworkStatus } from "@lume/utils"; import { cn, useNetworkStatus } from "@lume/utils";
import * as Avatar from "@radix-ui/react-avatar"; import * as Avatar from "@radix-ui/react-avatar";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
@@ -54,17 +54,27 @@ export function ActiveAccount() {
<DropdownMenu.Content <DropdownMenu.Content
side="right" side="right"
sideOffset={5} sideOffset={5}
className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-black/70 dark:bg-white/10 backdrop-blur-xl focus:outline-none" className="relative top-5 flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-white/50 dark:bg-black/50 ring-1 ring-black/10 dark:ring-white/10 backdrop-blur-2xl focus:outline-none"
> >
<DropdownMenu.Item asChild> <DropdownMenu.Item asChild>
<Link <Link
to="/settings/" to="/settings/profile"
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
<SettingsIcon className="size-5" /> <UserIcon className="size-4" />
Edit profile
</Link>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<Link
to="/settings/"
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<SettingsIcon className="size-4" />
Settings Settings
</Link> </Link>
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />
<Logout /> <Logout />
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Portal> </DropdownMenu.Portal>

View File

@@ -35,9 +35,9 @@ export function Logout() {
<AlertDialog.Trigger asChild> <AlertDialog.Trigger asChild>
<button <button
type="button" type="button"
className="inline-flex items-center gap-2 px-3 text-sm font-medium rounded-lg h-9 text-white/50 hover:bg-black/10 hover:text-white focus:outline-none dark:text-white/50 dark:hover:bg-white/10 dark:hover:text-white" className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
> >
<LogoutIcon className="size-5" /> <LogoutIcon className="size-4" />
Logout Logout
</button> </button>
</AlertDialog.Trigger> </AlertDialog.Trigger>

View File

@@ -106,7 +106,7 @@ const Image = ({ attributes, children, element }) => {
return ( return (
<div {...attributes}> <div {...attributes}>
{children} {children}
<div contentEditable={false} className="relative"> <div contentEditable={false} className="relative my-2">
<img <img
src={element.url} src={element.url}
alt={element.url} alt={element.url}
@@ -155,7 +155,7 @@ const Event = ({ attributes, element, children }) => {
<div <div
contentEditable={false} contentEditable={false}
onClick={() => Transforms.removeNodes(editor, { at: path })} onClick={() => Transforms.removeNodes(editor, { at: path })}
className="relative user-select-none" className="relative user-select-none my-2"
> >
<MentionNote <MentionNote
eventId={element.eventId.replace("nostr:", "")} eventId={element.eventId.replace("nostr:", "")}
@@ -319,11 +319,9 @@ export function EditorForm() {
setTarget(null); setTarget(null);
}} }}
> >
<div className="flex items-center justify-between h-16 px-3 border-b shrink-0 border-neutral-100 dark:border-neutral-900 bg-neutral-50 dark:bg-neutral-950"> <div className="flex items-center justify-between h-16 pl-7 pr-3 border-b shrink-0 border-neutral-100 dark:border-neutral-900 bg-neutral-50 dark:bg-neutral-950">
<div> <div>
<h3 className="font-semibold text-neutral-700 dark:text-neutral-500"> <h3 className="font-medium">New Post</h3>
New Post
</h3>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<div className="inline-flex items-center gap-2"> <div className="inline-flex items-center gap-2">
@@ -346,7 +344,7 @@ export function EditorForm() {
<div className="py-6 h-full overflow-y-auto px-7"> <div className="py-6 h-full overflow-y-auto px-7">
<Editable <Editable
key={JSON.stringify(editorValue)} key={JSON.stringify(editorValue)}
autoFocus={false} autoFocus={true}
autoCapitalize="none" autoCapitalize="none"
autoCorrect="none" autoCorrect="none"
spellCheck={false} spellCheck={false}
@@ -361,9 +359,9 @@ export function EditorForm() {
className="top-[-9999px] left-[-9999px] absolute z-10 w-[250px] p-1 bg-white border border-neutral-50 dark:border-neutral-900 dark:bg-neutral-950 rounded-lg shadow-lg" className="top-[-9999px] left-[-9999px] absolute z-10 w-[250px] p-1 bg-white border border-neutral-50 dark:border-neutral-900 dark:bg-neutral-950 rounded-lg shadow-lg"
> >
{filters.map((contact, i) => ( {filters.map((contact, i) => (
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> <button
<div
key={contact.npub} key={contact.npub}
type="button"
onClick={() => { onClick={() => {
Transforms.select(editor, target); Transforms.select(editor, target);
insertMention(editor, contact); insertMention(editor, contact);
@@ -379,7 +377,7 @@ export function EditorForm() {
</div> </div>
</User.Root> </User.Root>
</User.Provider> </User.Provider>
</div> </button>
))} ))}
</div> </div>
</Portal> </Portal>

View File

@@ -2,7 +2,7 @@ import { NDKCacheUserProfile } from "@lume/types";
import { ReactNode } from "react"; import { ReactNode } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { BaseEditor, Transforms } from "slate"; import { BaseEditor, Transforms } from "slate";
import { type ReactEditor } from "slate-react"; import { ReactEditor } from "slate-react";
export const Portal = ({ children }: { children?: ReactNode }) => { export const Portal = ({ children }: { children?: ReactNode }) => {
return typeof document === "object" return typeof document === "object"
@@ -36,6 +36,8 @@ export const insertImage = (editor: ReactEditor | BaseEditor, url: string) => {
}, },
]; ];
// @ts-ignore, idk
ReactEditor.focus(editor);
Transforms.insertNodes(editor, image); Transforms.insertNodes(editor, image);
Transforms.insertNodes(editor, extraText); Transforms.insertNodes(editor, extraText);
}; };
@@ -44,15 +46,24 @@ export const insertMention = (
editor: ReactEditor | BaseEditor, editor: ReactEditor | BaseEditor,
contact: NDKCacheUserProfile, contact: NDKCacheUserProfile,
) => { ) => {
const text = { text: "" };
const mention = { const mention = {
type: "mention", type: "mention",
npub: `nostr:${contact.npub}`, npub: `nostr:${contact.npub}`,
name: contact.name || contact.displayName || "anon", name: contact.name || contact.displayName || "anon",
children: [{ text: "" }], children: [text],
}; };
const extraText = [
{
type: "paragraph",
children: [text],
},
];
// @ts-ignore, idk
ReactEditor.focus(editor);
Transforms.insertNodes(editor, mention); Transforms.insertNodes(editor, mention);
Transforms.move(editor); Transforms.insertNodes(editor, extraText);
}; };
export const insertNostrEvent = ( export const insertNostrEvent = (

View File

@@ -2,11 +2,11 @@ import {
BellFilledIcon, BellFilledIcon,
BellIcon, BellIcon,
ComposeFilledIcon, ComposeFilledIcon,
ComposeIcon,
DepotFilledIcon, DepotFilledIcon,
DepotIcon, DepotIcon,
HomeFilledIcon, HomeFilledIcon,
HomeIcon, HomeIcon,
PlusIcon,
SearchFilledIcon, SearchFilledIcon,
SearchIcon, SearchIcon,
SettingsFilledIcon, SettingsFilledIcon,
@@ -44,11 +44,7 @@ export function Navigation() {
: "bg-black/5 hover:bg-blue-500 dark:bg-white/5 dark:hover:bg-blue-500", : "bg-black/5 hover:bg-blue-500 dark:bg-white/5 dark:hover:bg-blue-500",
)} )}
> >
{isEditorOpen ? ( <PlusIcon className="size-5" />
<ComposeFilledIcon className="size-5" />
) : (
<ComposeIcon className="size-5" />
)}
</button> </button>
</div> </div>
<div className="my-5 w-2/3 mx-auto h-px bg-black/10 dark:bg-white/10" /> <div className="my-5 w-2/3 mx-auto h-px bg-black/10 dark:bg-white/10" />

View File

@@ -57,8 +57,8 @@ export function OnboardingInterestScreen() {
<div className="w-full flex-1 min-h-0 flex flex-col justify-between"> <div className="w-full flex-1 min-h-0 flex flex-col justify-between">
<div className="flex-1 min-h-0 overflow-y-auto px-8 py-8"> <div className="flex-1 min-h-0 overflow-y-auto px-8 py-8">
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
{TOPICS.map((topic, index) => ( {TOPICS.map((topic) => (
<div key={topic.title + index} className="flex flex-col gap-4"> <div key={topic.title} className="flex flex-col gap-4">
<div className="w-full flex items-center justify-between"> <div className="w-full flex items-center justify-between">
<div className="inline-flex items-center gap-2.5"> <div className="inline-flex items-center gap-2.5">
<img <img
@@ -79,6 +79,7 @@ export function OnboardingInterestScreen() {
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
{topic.content.map((hashtag) => ( {topic.content.map((hashtag) => (
<button <button
key={hashtag}
type="button" type="button"
onClick={() => toggleHashtag(hashtag)} onClick={() => toggleHashtag(hashtag)}
className={cn( className={cn(

View File

@@ -35,19 +35,17 @@ export function OnboardingProfileScreen() {
navigate("/interests"); navigate("/interests");
} }
const oldProfile = await ark.getUserProfile(); const prevProfile = await ark.getUserProfile();
const newProfile: NDKUserProfile = {
const profile: NDKUserProfile = {
...data, ...data,
lud16: "", // temporary remove lud16 nip05: prevProfile?.nip05 || "",
nip05: oldProfile?.nip05 || "",
bio: data.about, bio: data.about,
image: picture, image: picture,
picture: picture, picture: picture,
}; };
const publish = await ark.createEvent({ const publish = await ark.createEvent({
content: JSON.stringify(profile), content: JSON.stringify(newProfile),
kind: NDKKind.Metadata, kind: NDKKind.Metadata,
tags: [], tags: [],
}); });
@@ -57,7 +55,7 @@ export function OnboardingProfileScreen() {
await storage.clearProfileCache(ark.account.pubkey); await storage.clearProfileCache(ark.account.pubkey);
await queryClient.setQueryData( await queryClient.setQueryData(
["user", ark.account.pubkey], ["user", ark.account.pubkey],
() => profile, () => newProfile,
); );
setLoading(false); setLoading(false);

View File

@@ -11,7 +11,7 @@ export function EventRoute() {
return ( return (
<div className="pb-5 overflow-y-auto"> <div className="pb-5 overflow-y-auto">
<WindowVirtualizer> <WindowVirtualizer>
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center justify-start gap-2 px-3 border-neutral-100 dark:border-neutral-900 mb-3"> <div className="relative z-50 h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center justify-start gap-2 px-3 border-neutral-100 dark:border-neutral-900 mb-3">
<button <button
type="button" type="button"
className="size-9 hover:bg-neutral-100 hover:text-blue-500 dark:hover:bg-neutral-900 rounded-lg inline-flex items-center justify-center" className="size-9 hover:bg-neutral-100 hover:text-blue-500 dark:hover:bg-neutral-900 rounded-lg inline-flex items-center justify-center"

View File

@@ -114,11 +114,11 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
</div> </div>
<div className="flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900"> <div className="flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900">
{isLoading ? ( {isLoading ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-44 w-full items-center justify-center">
<LoaderIcon className="size-4 animate-spin" /> <LoaderIcon className="size-4 animate-spin" />
</div> </div>
) : isError ? ( ) : isError ? (
<div className="flex h-full w-full items-center justify-center"> <div className="flex h-44 w-full items-center justify-center">
Error. Cannot get trending users Error. Cannot get trending users
</div> </div>
) : ( ) : (
@@ -171,9 +171,9 @@ export function SuggestRoute({ queryKey }: { queryKey: string[] }) {
type="button" type="button"
onClick={submit} onClick={submit}
disabled={loading} disabled={loading}
className="inline-flex items-center justify-center gap-2 px-6 font-medium text-white transform bg-blue-500 rounded-full active:translate-y-1 w-36 h-11 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed" className="inline-flex items-center justify-center gap-2 px-6 font-medium shadow-xl shadow-neutral-500/50 text-white transform bg-blue-500 rounded-full active:translate-y-1 w-36 h-11 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed"
> >
Save Save & Go Back
</button> </button>
</div> </div>
</div> </div>

View File

@@ -68,9 +68,9 @@ export function TranslateRegisterModal({ setAPIKey }) {
<div className="flex-1 min-h-0 flex flex-col justify-between px-8 py-8"> <div className="flex-1 min-h-0 flex flex-col justify-between px-8 py-8">
<div className="flex flex-col gap-1.5"> <div className="flex flex-col gap-1.5">
<p className="text-sm text-neutral-500"> <p className="text-sm text-neutral-500">
Translate Service is provided by{" "} Translation Service is provided by{" "}
<span className="text-blue-500">nostr.wine</span>, you need to <span className="text-blue-500">nostr.wine</span>. Prices
deposit at least 2,500 sats to use translate 50,000 characters start at 2,500 sats for 50,000 characters of translated text.
</p> </p>
<p className="text-sm text-neutral-500"> <p className="text-sm text-neutral-500">
You can learn more about nostr.wine{" "} You can learn more about nostr.wine{" "}

View File

@@ -5,13 +5,9 @@ export function TutorialManageColumnScreen() {
<div className="px-5 h-full flex flex-col justify-between"> <div className="px-5 h-full flex flex-col justify-between">
<div className="h-full min-h-0 flex flex-col gap-2"> <div className="h-full min-h-0 flex flex-col gap-2">
<p> <p>
Lume is also provide simple way to customize column after creation. Once a new column is created, you can click on the title in its header
to find options to <span className="font-semibold">customize</span> it
</p> </p>
<p>
<span className="font-semibold">To customize each column,</span> you
can go to header of each column
</p>
<p>Click to "Three Dots" icon</p>
<img <img
src="/tutorial-3.gif" src="/tutorial-3.gif"
alt="tutorial-3" alt="tutorial-3"

View File

@@ -4,10 +4,10 @@ export function TutorialNewColumnScreen() {
return ( return (
<div className="px-5 h-full flex flex-col justify-between"> <div className="px-5 h-full flex flex-col justify-between">
<div className="h-full min-h-0 flex flex-col gap-2"> <div className="h-full min-h-0 flex flex-col gap-2">
<p>Lume is column based, each column is each experience</p> <p>Lume is column based, each column is its own experience.</p>
<p> <p>
<span className="font-semibold">To create new column,</span> you can <span className="font-semibold">To create a new column</span>, you can
look into bottom right part of screen click on the "Plus" icon at bottom right corner of this window.
</p> </p>
<p>Click to "Plus" icon</p> <p>Click to "Plus" icon</p>
<img <img

View File

@@ -8,13 +8,13 @@
"access": "public" "access": "public"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.17.15", "@tanstack/react-query": "^5.17.19",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"jotai": "^2.6.2", "jotai": "^2.6.3",
"nostr-tools": "1.17.0", "nostr-tools": "1.17.0",
"react": "^18.2.0", "react": "^18.2.0",
"tailwind-merge": "^2.2.0" "tailwind-merge": "^2.2.1"
}, },
"devDependencies": { "devDependencies": {
"@lume/tsconfig": "workspace:^", "@lume/tsconfig": "workspace:^",

695
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@
"withGlobalTauri": true "withGlobalTauri": true
}, },
"package": { "package": {
"productName": "Lume 3", "productName": "Lume",
"version": "3.0.0" "version": "3.0.0-beta.1"
}, },
"plugins": { "plugins": {
"fs": { "fs": {
@@ -62,7 +62,7 @@
"copyright": "", "copyright": "",
"identifier": "nu.lume", "identifier": "nu.lume",
"longDescription": "nostr client for desktop", "longDescription": "nostr client for desktop",
"shortDescription": "", "shortDescription": "nostr client",
"targets": "all", "targets": "all",
"updater": { "updater": {
"active": true, "active": true,