feat: onboarding

This commit is contained in:
reya 2024-05-09 15:06:42 +07:00
parent c8e1b8b8bd
commit 777eb15b4f
18 changed files with 562 additions and 63 deletions

BIN
apps/desktop2/public/ai.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View File

@ -25,6 +25,7 @@ interface RouterContext {
// Profile // Profile
accounts?: string[]; accounts?: string[];
profile?: Metadata; profile?: Metadata;
isNewUser?: boolean;
// Editor // Editor
initialValue?: EditorElement[]; initialValue?: EditorElement[];
} }

View File

@ -1,5 +1,6 @@
import { CheckIcon } from "@lume/icons"; import { CheckIcon } from "@lume/icons";
import type { AppRouteSearch } from "@lume/types"; import type { AppRouteSearch } from "@lume/types";
import { Spinner } from "@lume/ui";
import { displayNsec } from "@lume/utils"; import { displayNsec } from "@lume/utils";
import * as Checkbox from "@radix-ui/react-checkbox"; import * as Checkbox from "@radix-ui/react-checkbox";
import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { createFileRoute, useNavigate } from "@tanstack/react-router";
@ -25,6 +26,7 @@ function Screen() {
const [key, setKey] = useState(null); const [key, setKey] = useState(null);
const [passphase, setPassphase] = useState(""); const [passphase, setPassphase] = useState("");
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [loading, setLoading] = useState(false);
const [confirm, setConfirm] = useState({ c1: false, c2: false, c3: false }); const [confirm, setConfirm] = useState({ c1: false, c2: false, c3: false });
const navigate = useNavigate(); const navigate = useNavigate();
@ -42,13 +44,19 @@ function Screen() {
}); });
} }
const encrypted: string = await invoke("get_encrypted_key", { // start loading
setLoading(true);
invoke("get_encrypted_key", {
npub: account, npub: account,
password: passphase, password: passphase,
}).then((encrypted: string) => {
// update state
setKey(encrypted);
setLoading(false);
}); });
setKey(encrypted);
} catch (e) { } catch (e) {
setLoading(false);
toast.error(String(e)); toast.error(String(e));
} }
}; };
@ -180,9 +188,10 @@ function Screen() {
<button <button
type="button" type="button"
onClick={() => submit()} onClick={() => submit()}
disabled={loading}
className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50" className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
> >
{t("global.continue")} {loading ? <Spinner /> : t("global.continue")}
</button> </button>
</div> </div>
</div> </div>

View File

@ -85,8 +85,11 @@ function Screen() {
const eventId = await ark.set_settings(newSettings); const eventId = await ark.set_settings(newSettings);
if (eventId) { if (eventId) {
console.log("event_id: ", eventId); return navigate({
navigate({ to: "/$account/home", params: { account }, replace: true }); to: "/$account/home",
params: { account },
replace: true,
});
} }
} catch (e) { } catch (e) {
setLoading(false); setLoading(false);

View File

@ -6,7 +6,8 @@ import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons";
import { type ColumnRouteSearch, type Event, Kind } from "@lume/types"; import { type ColumnRouteSearch, type Event, Kind } from "@lume/types";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { Link, createFileRoute } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
export const Route = createFileRoute("/newsfeed")({ export const Route = createFileRoute("/newsfeed")({
@ -121,8 +122,6 @@ export function Screen() {
} }
function Empty() { function Empty() {
const search = Route.useSearch();
return ( return (
<div className="flex flex-col py-10 gap-10"> <div className="flex flex-col py-10 gap-10">
<div className="text-center flex flex-col items-center justify-center"> <div className="text-center flex flex-col items-center justify-center">
@ -135,29 +134,19 @@ function Empty() {
</p> </p>
</div> </div>
<div className="flex flex-col px-3 gap-2"> <div className="flex flex-col px-3 gap-2">
<Link
to="/global"
search={search}
className="h-11 w-full flex items-center justify-between bg-black/10 hover:bg-black/20 text-sm font-medium dark:bg-white/10 dark:hover:bg-white/20 gap-2 rounded-lg px-3"
>
Show global newsfeed
<ArrowRightIcon className="size-5" />
</Link>
<Link <Link
to="/trending/notes" to="/trending/notes"
search={search} className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
className="h-11 w-full flex items-center justify-between bg-black/10 hover:bg-black/20 text-sm font-medium dark:bg-white/10 dark:hover:bg-white/20 gap-2 rounded-lg px-3"
> >
Show trending notes
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
Show trending notes
</Link> </Link>
<Link <Link
to="/trending/users" to="/trending/users"
search={search} className="h-11 w-full flex items-center hover:bg-neutral-200 text-sm font-medium dark:hover:bg-neutral-800 gap-2 bg-neutral-100 rounded-lg dark:bg-neutral-900 px-3"
className="h-11 w-full flex items-center justify-between bg-black/10 hover:bg-black/20 text-sm font-medium dark:bg-white/10 dark:hover:bg-white/20 gap-2 rounded-lg px-3"
> >
Discover trending users
<ArrowRightIcon className="size-5" /> <ArrowRightIcon className="size-5" />
Discover trending users
</Link> </Link>
</div> </div>
</div> </div>

View File

@ -0,0 +1,445 @@
import { ArrowRightIcon, CancelIcon } from "@lume/icons";
import type { ColumnRouteSearch, LumeColumn } from "@lume/types";
import { Spinner, User } from "@lume/ui";
import { cn } from "@lume/utils";
import { createFileRoute } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { getCurrent } from "@tauri-apps/api/window";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
export const Route = createFileRoute("/onboarding")({
validateSearch: (search: Record<string, string>): ColumnRouteSearch => {
return {
account: search.account,
label: search.label,
name: search.name,
};
},
component: Screen,
});
function Screen() {
const { label } = Route.useSearch();
const {
register,
handleSubmit,
reset,
formState: { isValid, isSubmitting },
} = useForm();
const [userType, setUserType] = useState<"new" | "veteran">(null);
const install = async (column: LumeColumn) => {
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "add", column });
};
const close = async () => {
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "remove", label });
};
const friendToFriend = async (data: { npub: string }) => {
if (!data.npub.startsWith("npub1"))
return toast.warning(
"NPUB is invalid. NPUB must be starts with npub1...",
);
try {
const connect: boolean = await invoke("friend_to_friend", {
npub: data.npub,
});
if (connect) {
const column = {
label: "newsfeed",
name: "Newsfeed",
content: "/newsfeed",
};
const mainWindow = getCurrent();
await mainWindow.emit("columns", { type: "add", column });
// reset form
reset();
}
} catch (e) {
toast.error(String(e));
}
};
return (
<div className="h-full flex flex-col py-6 gap-6 overflow-y-auto scrollbar-none">
<div className="text-center flex flex-col items-center justify-center">
<h1 className="text-2xl font-serif font-medium">Welcome to Lume</h1>
<p className="leading-tight text-neutral-700 dark:text-neutral-300">
Here are a few suggestions to help you get started.
</p>
</div>
<div className="px-3">
<div className="mb-6 w-full h-44">
<img
src="/lock-screen.jpg"
srcSet="/lock-screen@2x.jpg 2x"
alt="background"
className="h-full w-full object-cover rounded-xl outline outline-1 -outline-offset-1 outline-black/15"
/>
</div>
<div className="flex flex-col gap-6">
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
👋 Yo! I'm Mide, and I'll be your friendly guide to Nostr and
beyond. Looking forward to our adventure together!
</div>
</div>
</div>
<div className="flex items-start flex-row-reverse gap-2 text-[13px]">
<CurrentUser />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold text-end">You</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
How can I get started?
</div>
<button
type="button"
onClick={() => setUserType("new")}
className={cn(
"mt-1 px-3 py-2 shadow-primary flex items-center justify-between gap-6 bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg",
userType === "new"
? "bg-blue-500 text-white hover:bg-blue-600"
: "",
)}
>
I'm completely new to Nostr.
<ArrowRightIcon className="size-4" />
</button>
<button
type="button"
onClick={() => setUserType("veteran")}
className={cn(
"mt-1 px-3 py-2 shadow-primary flex items-center justify-between gap-6 bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg",
userType === "veteran"
? "bg-blue-500 text-white hover:bg-blue-600"
: "",
)}
>
I've already been using another Nostr client.
<ArrowRightIcon className="size-4" />
</button>
</div>
</div>
{userType === "veteran" ? (
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
So, I'm excited to give you a quick intro to Lume and all the
awesome features it has to offer. Let's dive in!
</div>
</div>
</div>
) : null}
{userType === "veteran" ? (
<div className="flex items-start flex-row-reverse gap-2 text-[13px]">
<CurrentUser />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold text-end">You</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Thanks! But I already know about Lume.
</div>
<button
type="button"
onClick={() =>
install({
label: "newsfeed",
name: "Newsfeed",
content: "/newsfeed",
})
}
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between gap-6 bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
>
Skip! Show my newsfeed
<ArrowRightIcon className="size-4" />
</button>
</div>
</div>
) : null}
{userType === "veteran" ? (
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
First off, Lume is a social media client for Nostr. It's a
place where you can follow friends, dive into chats, and post
what's on your mind.
</div>
</div>
</div>
) : null}
{userType === "veteran" ? (
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
That's not all! What makes Lume unique is the column system.
You can enhance your experience by adding new columns from the
Lume Store.
</div>
<div className="mt-1 p-2 bg-black/5 dark:bg-white/5 rounded-lg">
If you're confused about the term "Column," you can imagine it
as mini-apps, with each column providing its own experience.
</div>
<div className="mt-1 p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Here is a quick guide for how to add a new column:
</div>
<div className="mt-1 rounded-lg">
<video
className="h-auto w-full rounded-lg object-cover aspect-video outline outline-1 -outline-offset-1 outline-black/15"
controls
muted
>
<source
src="https://samplelib.com/lib/preview/mp4/sample-5s.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</div>
</div>
) : null}
{userType === "veteran" ? (
<div className="flex items-start flex-row-reverse gap-2 text-[13px]">
<CurrentUser />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold text-end">You</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Can you introduce me to the UI? I am still confused.
</div>
</div>
</div>
) : null}
{userType === "veteran" ? (
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Of course, here is a quick introduction video for Lume.
</div>
<div className="mt-1 rounded-lg">
<video
className="h-auto w-full rounded-lg object-cover aspect-video outline outline-1 -outline-offset-1 outline-black/15"
controls
muted
>
<source
src="https://samplelib.com/lib/preview/mp4/sample-5s.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
</div>
</div>
</div>
) : null}
{userType === "new" ? (
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Diving into new social media platforms like Nostr can be a bit
overwhelming, but don't worry! Here are some handy tips to
help you navigate and discover what interests you.
</div>
<button
type="button"
onClick={() =>
install({
label: "foryou",
name: "For you",
content: "/foryou",
})
}
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
>
Add some topics that you're interested in.
<ArrowRightIcon className="size-4" />
</button>
<button
type="button"
onClick={() =>
install({
label: "trending_users",
name: "Trending",
content: "/trending/users",
})
}
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
>
Follow some users.
<ArrowRightIcon className="size-4" />
</button>
</div>
</div>
) : null}
{userType === "new" ? (
<div className="flex items-start flex-row-reverse gap-2 text-[13px]">
<CurrentUser />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold text-end">You</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
My girlfriend introduced Nostr to me, and I have her NPUB. Can
I get the same experiences as her?
</div>
</div>
</div>
) : null}
{userType === "new" ? (
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Absolutely! Since your girlfriend shared her NPUB with you,
you can dive into Nostr and explore it just like she does.
It's a great way to share experiences and discover what Nostr
has to offer together!
</div>
<form
onSubmit={handleSubmit(friendToFriend)}
className="mt-1 flex flex-col items-end bg-white dark:bg-white/10 rounded-lg shadow-primary"
>
<input
{...register("npub", { required: true })}
name="npub"
placeholder="Enter npub here..."
className="w-full h-14 px-3 rounded-t-lg bg-transparent border-b border-x-0 border-t-0 border-neutral-100 dark:border-white/5 focus:border-neutral-200 dark:focus:border-white/20 focus:outline-none focus:ring-0 placeholder:text-neutral-600 dark:placeholder:text-neutral-400"
/>
<div className="h-10 flex items-center px-1">
<button
type="submit"
disabled={!isValid || isSubmitting}
className="px-2 h-8 w-20 inline-flex items-center justify-center bg-blue-500 text-white rounded-md text-sm font-medium hover:bg-blue-600"
>
{isSubmitting ? <Spinner className="size-4" /> : "Submit"}
</button>
</div>
</form>
</div>
</div>
) : null}
{userType ? (
<>
<div className="flex items-start flex-row-reverse gap-2 text-[13px]">
<CurrentUser />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold text-end">You</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
Thank you. I can use Lume and explore Nostr by myself from
now on.
</div>
</div>
</div>
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
I really hope you enjoy your time on Nostr! If you're keen
to dive deeper, here are some helpful resources to get you
started:
</div>
<a
href="https://nostr.org"
target="_blank"
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
rel="noreferrer"
>
[Website] nostr.org
<ArrowRightIcon className="size-4" />
</a>
<a
href="https://www.youtube.com/watch?v=5W-jtbbh3eA"
target="_blank"
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
rel="noreferrer"
>
[Video] What is Nostr?
<ArrowRightIcon className="size-4" />
</a>
<a
href="https://github.com/nostr-protocol/nostr"
target="_blank"
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
rel="noreferrer"
>
[Develop] Github
<ArrowRightIcon className="size-4" />
</a>
<a
href="https://www.nostrapps.com/"
target="_blank"
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
rel="noreferrer"
>
[Ecosystem] nostrapps.com
<ArrowRightIcon className="size-4" />
</a>
</div>
</div>
<div className="flex items-start gap-2 text-[13px]">
<Mide />
<div className="flex flex-col gap-0.5">
<h3 className="font-semibold">Mide</h3>
<div className="p-2 bg-black/5 dark:bg-white/5 rounded-lg">
If you want to close this onboarding board, you can click
the button below.
</div>
<button
type="button"
onClick={() => close()}
className="mt-1 px-3 py-2 shadow-primary flex items-center justify-between bg-white hover:bg-blue-500 hover:text-white dark:bg-white/10 rounded-lg"
>
Close
<CancelIcon className="size-4" />
</button>
</div>
</div>
</>
) : null}
</div>
</div>
</div>
);
}
function Mide() {
return (
<img
src="/ai.jpg"
alt="Ai-chan"
className="shrink-0 size-10 rounded-full outline outline-1 -outline-offset-1 outline-black/15"
/>
);
}
function CurrentUser() {
const { account } = Route.useSearch();
return (
<User.Provider pubkey={account}>
<User.Root className="shrink-0">
<User.Avatar className="size-10 rounded-full outline outline-1 -outline-offset-1 outline-black/15" />
</User.Root>
</User.Provider>
);
}

View File

@ -5,7 +5,6 @@ import { getCurrent } from "@tauri-apps/api/window";
import { readTextFile } from "@tauri-apps/plugin-fs"; import { readTextFile } from "@tauri-apps/plugin-fs";
export const Route = createFileRoute("/store/official")({ export const Route = createFileRoute("/store/official")({
component: Screen,
beforeLoad: async () => { beforeLoad: async () => {
const resourcePath = await resolveResource( const resourcePath = await resolveResource(
"resources/official_columns.json", "resources/official_columns.json",
@ -18,6 +17,7 @@ export const Route = createFileRoute("/store/official")({
officialColumns, officialColumns,
}; };
}, },
component: Screen,
}); });
function Screen() { function Screen() {

View File

@ -11,7 +11,7 @@
"tauri": "tauri" "tauri": "tauri"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.7.1", "@biomejs/biome": "1.7.3",
"@tauri-apps/cli": "2.0.0-beta.12", "@tauri-apps/cli": "2.0.0-beta.12",
"turbo": "^1.13.3" "turbo": "^1.13.3"
}, },

View File

@ -1,10 +1,10 @@
import { import {
Kind,
type Contact, type Contact,
type Event, type Event,
type EventWithReplies, type EventWithReplies,
type Interests, type Interests,
type Keys, type Keys,
Kind,
type LumeColumn, type LumeColumn,
type Metadata, type Metadata,
type Settings, type Settings,
@ -194,8 +194,10 @@ export class Ark {
.filter((el) => el[0] === "e") .filter((el) => el[0] === "e")
?.map((item) => item[1]); ?.map((item) => item[1]);
if (eventIds && eventIds.length) { if (eventIds?.length) {
eventIds.forEach((id) => seenIds.add(id)); for (const id of eventIds) {
seenIds.add(id);
}
} }
} }

View File

@ -13,6 +13,7 @@
"@evilmartians/harmony": "^1.2.0", "@evilmartians/harmony": "^1.2.0",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"tailwind-gradient-mask-image": "^1.2.0",
"tailwind-scrollbar": "^3.1.0", "tailwind-scrollbar": "^3.1.0",
"tailwindcss": "^3.4.3" "tailwindcss": "^3.4.3"
} }

View File

@ -38,7 +38,6 @@
"slate-react": "^0.102.0", "slate-react": "^0.102.0",
"sonner": "^1.4.41", "sonner": "^1.4.41",
"string-strip-html": "^13.4.8", "string-strip-html": "^13.4.8",
"tailwind-gradient-mask-image": "^1.2.0",
"uqr": "^0.1.2", "uqr": "^0.1.2",
"use-debounce": "^10.0.0", "use-debounce": "^10.0.0",
"virtua": "^0.30.2" "virtua": "^0.30.2"

64
pnpm-lock.yaml generated
View File

@ -46,8 +46,8 @@ importers:
version: 2.0.0-beta.3 version: 2.0.0-beta.3
devDependencies: devDependencies:
'@biomejs/biome': '@biomejs/biome':
specifier: ^1.7.1 specifier: 1.7.3
version: 1.7.1 version: 1.7.3
'@tauri-apps/cli': '@tauri-apps/cli':
specifier: 2.0.0-beta.12 specifier: 2.0.0-beta.12
version: 2.0.0-beta.12 version: 2.0.0-beta.12
@ -283,6 +283,9 @@ importers:
'@tailwindcss/typography': '@tailwindcss/typography':
specifier: ^0.5.13 specifier: ^0.5.13
version: 0.5.13(tailwindcss@3.4.3) version: 0.5.13(tailwindcss@3.4.3)
tailwind-gradient-mask-image:
specifier: ^1.2.0
version: 1.2.0
tailwind-scrollbar: tailwind-scrollbar:
specifier: ^3.1.0 specifier: ^3.1.0
version: 3.1.0(tailwindcss@3.4.3) version: 3.1.0(tailwindcss@3.4.3)
@ -402,9 +405,6 @@ importers:
string-strip-html: string-strip-html:
specifier: ^13.4.8 specifier: ^13.4.8
version: 13.4.8 version: 13.4.8
tailwind-gradient-mask-image:
specifier: ^1.2.0
version: 1.2.0
uqr: uqr:
specifier: ^0.1.2 specifier: ^0.1.2
version: 0.1.2 version: 0.1.2
@ -921,24 +921,24 @@ packages:
'@babel/helper-validator-identifier': 7.24.5 '@babel/helper-validator-identifier': 7.24.5
to-fast-properties: 2.0.0 to-fast-properties: 2.0.0
/@biomejs/biome@1.7.1: /@biomejs/biome@1.7.3:
resolution: {integrity: sha512-wb2UNoFXcgaMdKXKT5ytsYntaogl2FSTjDt20CZynF3v7OXQUcIpTrr+be3XoOGpoZRj3Ytq9TSpmplUREXmeA==} resolution: {integrity: sha512-ogFQI+fpXftr+tiahA6bIXwZ7CSikygASdqMtH07J2cUzrpjyTMVc9Y97v23c7/tL1xCZhM+W9k4hYIBm7Q6cQ==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
optionalDependencies: optionalDependencies:
'@biomejs/cli-darwin-arm64': 1.7.1 '@biomejs/cli-darwin-arm64': 1.7.3
'@biomejs/cli-darwin-x64': 1.7.1 '@biomejs/cli-darwin-x64': 1.7.3
'@biomejs/cli-linux-arm64': 1.7.1 '@biomejs/cli-linux-arm64': 1.7.3
'@biomejs/cli-linux-arm64-musl': 1.7.1 '@biomejs/cli-linux-arm64-musl': 1.7.3
'@biomejs/cli-linux-x64': 1.7.1 '@biomejs/cli-linux-x64': 1.7.3
'@biomejs/cli-linux-x64-musl': 1.7.1 '@biomejs/cli-linux-x64-musl': 1.7.3
'@biomejs/cli-win32-arm64': 1.7.1 '@biomejs/cli-win32-arm64': 1.7.3
'@biomejs/cli-win32-x64': 1.7.1 '@biomejs/cli-win32-x64': 1.7.3
dev: true dev: true
/@biomejs/cli-darwin-arm64@1.7.1: /@biomejs/cli-darwin-arm64@1.7.3:
resolution: {integrity: sha512-qfLrIIB58dkgiY/1tgG6fSCBK22PZaSIf6blweZBsG6iMij05mEuJt50ne+zPnNFNUmt8t43NC/qOXT3iFHQBA==} resolution: {integrity: sha512-eDvLQWmGRqrPIRY7AIrkPHkQ3visEItJKkPYSHCscSDdGvKzYjmBJwG1Gu8+QC5ed6R7eiU63LEC0APFBobmfQ==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
@ -946,8 +946,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@biomejs/cli-darwin-x64@1.7.1: /@biomejs/cli-darwin-x64@1.7.3:
resolution: {integrity: sha512-OGeyNsEcp5VnKbF9/TBjPCTHNEOm7oHegEve07U3KZmzqfpw2Oe3i9DVW8t6vvj1TYbrwWYCld25H34kBDY7Vg==} resolution: {integrity: sha512-JXCaIseKRER7dIURsVlAJacnm8SG5I0RpxZ4ya3dudASYUc68WGl4+FEN03ABY3KMIq7hcK1tzsJiWlmXyosZg==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
@ -955,8 +955,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@biomejs/cli-linux-arm64-musl@1.7.1: /@biomejs/cli-linux-arm64-musl@1.7.3:
resolution: {integrity: sha512-giH0/CzLOJ+wbxLxd5Shnr5xQf5fGnTRWLDe3lzjaF7IplVydNCEeZJtncB01SvyA6DAFJsvQ4LNxzAOQfEVCg==} resolution: {integrity: sha512-c8AlO45PNFZ1BYcwaKzdt46kYbuP6xPGuGQ6h4j3XiEDpyseRRUy/h+6gxj07XovmyxKnSX9GSZ6nVbZvcVUAw==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
@ -964,8 +964,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@biomejs/cli-linux-arm64@1.7.1: /@biomejs/cli-linux-arm64@1.7.3:
resolution: {integrity: sha512-MQDf5wErj1iBvlcxCyOa0XqZYN8WJrupVgbNnqhntO3yVATg8GxduVUn1fDSaolznkDRsj7Pz3Xu1esBFwvfmg==} resolution: {integrity: sha512-phNTBpo7joDFastnmZsFjYcDYobLTx4qR4oPvc9tJ486Bd1SfEVPHEvJdNJrMwUQK56T+TRClOQd/8X1nnjA9w==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
@ -973,8 +973,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@biomejs/cli-linux-x64-musl@1.7.1: /@biomejs/cli-linux-x64-musl@1.7.3:
resolution: {integrity: sha512-ySNDtPhsLxU125IFHHAxfpoHBpkM56s4mEXeO70GZtgZay/o1h8IUPWCWf5Z7gKgc4jwgYN1U1U9xabI3hZVAg==} resolution: {integrity: sha512-UdEHKtYGWEX3eDmVWvQeT+z05T9/Sdt2+F/7zmMOFQ7boANeX8pcO6EkJPK3wxMudrApsNEKT26rzqK6sZRTRA==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
@ -982,8 +982,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@biomejs/cli-linux-x64@1.7.1: /@biomejs/cli-linux-x64@1.7.3:
resolution: {integrity: sha512-3wmCsGcC3KZ4pfTknXHfyMMlXPMhgfXVAcG5GlrR+Tq2JGiAw0EUydaLpsSBEbcG7IxH6OiUZEJZ95kAycCHBA==} resolution: {integrity: sha512-vnedYcd5p4keT3iD48oSKjOIRPYcjSNNbd8MO1bKo9ajg3GwQXZLAH+0Cvlr+eMsO67/HddWmscSQwTFrC/uPA==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
@ -991,8 +991,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@biomejs/cli-win32-arm64@1.7.1: /@biomejs/cli-win32-arm64@1.7.3:
resolution: {integrity: sha512-8hIDakEqZn0i6+388noYKdZ0ZrovTwnvMU/Qp/oJou0G7EPVdXupOe0oxiQSdRN0W7f6CS/yjPCYuVGzDG6r0g==} resolution: {integrity: sha512-unNCDqUKjujYkkSxs7gFIfdasttbDC4+z0kYmcqzRk6yWVoQBL4dNLcCbdnJS+qvVDNdI9rHp2NwpQ0WAdla4Q==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
@ -1000,8 +1000,8 @@ packages:
dev: true dev: true
optional: true optional: true
/@biomejs/cli-win32-x64@1.7.1: /@biomejs/cli-win32-x64@1.7.3:
resolution: {integrity: sha512-3W9k3uH6Ea6VOpAS9xkkAlS0LTfnGQjmIUCegZ8SDtK2NgJ1gO+qdEkGJb0ltahusFTN1QxJ107dM7ASA9IUEg==} resolution: {integrity: sha512-ZmByhbrnmz/UUFYB622CECwhKIPjJLLPr5zr3edhu04LzbfcOrz16VYeNq5dpO1ADG70FORhAJkaIGdaVBG00w==}
engines: {node: '>=14.21.3'} engines: {node: '>=14.21.3'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@ -5912,7 +5912,7 @@ packages:
/tailwind-gradient-mask-image@1.2.0: /tailwind-gradient-mask-image@1.2.0:
resolution: {integrity: sha512-tUJaGhvqbJFiVKJu6EU5n//KvGdVvY3L3VOFNqjztk13+ifAk00pcSNHBTgHfUiBGOEzDn0gFRbSmsftUV1lXA==} resolution: {integrity: sha512-tUJaGhvqbJFiVKJu6EU5n//KvGdVvY3L3VOFNqjztk13+ifAk00pcSNHBTgHfUiBGOEzDn0gFRbSmsftUV1lXA==}
dev: false dev: true
/tailwind-merge@2.3.0: /tailwind-merge@2.3.0:
resolution: {integrity: sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==} resolution: {integrity: sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==}

View File

@ -1,4 +1,14 @@
[ [
{
"label": "lZfXLFgPPR4NNrgjlWDxn",
"name": "Newsfeed",
"content": "/newsfeed",
"logo": "",
"cover": "/newsfeed.png",
"coverRetina": "/newsfeed@2x.png",
"author": "Lume",
"description": "Keep up to date with people you're following."
},
{ {
"label": "rRtguZwIpd5G8Wt54OTb7", "label": "rRtguZwIpd5G8Wt54OTb7",
"name": "For you", "name": "For you",

View File

@ -1,4 +1,4 @@
[ [
{ "label": "home", "name": "Home", "content": "/newsfeed" }, { "label": "onboarding", "name": "Onboarding", "content": "/onboarding" },
{ "label": "open", "name": "Open", "content": "/open" } { "label": "open", "name": "Open", "content": "/open" }
] ]

View File

@ -121,6 +121,7 @@ fn main() {
nostr::metadata::get_balance, nostr::metadata::get_balance,
nostr::metadata::zap_profile, nostr::metadata::zap_profile,
nostr::metadata::zap_event, nostr::metadata::zap_event,
nostr::metadata::friend_to_friend,
nostr::event::get_event, nostr::event::get_event,
nostr::event::get_events_from, nostr::event::get_events_from,
nostr::event::get_events, nostr::event::get_events,

View File

@ -82,6 +82,45 @@ pub async fn get_activities(
} }
} }
#[tauri::command]
pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
match PublicKey::from_bech32(npub) {
Ok(author) => {
let mut contact_list: Vec<Contact> = Vec::new();
let contact_list_filter = Filter::new()
.author(author)
.kind(Kind::ContactList)
.limit(1);
if let Ok(contact_list_events) = client.get_events_of(vec![contact_list_filter], None).await {
for event in contact_list_events.into_iter() {
for tag in event.into_iter_tags() {
if let Tag::PublicKey {
public_key,
relay_url,
alias,
uppercase: false,
} = tag
{
contact_list.push(Contact::new(public_key, relay_url, alias))
}
}
}
}
println!("contact list: {}", contact_list.len());
match client.set_contact_list(contact_list).await {
Ok(_) => Ok(true),
Err(err) => Err(err.to_string()),
}
}
Err(err) => Err(err.to_string()),
}
}
#[tauri::command] #[tauri::command]
pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result<Metadata, String> { pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result<Metadata, String> {
let client = &state.client; let client = &state.client;