diff --git a/apps/desktop2/public/ai.jpg b/apps/desktop2/public/ai.jpg new file mode 100644 index 00000000..36843631 Binary files /dev/null and b/apps/desktop2/public/ai.jpg differ diff --git a/apps/desktop2/public/newsfeed.png b/apps/desktop2/public/newsfeed.png new file mode 100644 index 00000000..4e0a06ad Binary files /dev/null and b/apps/desktop2/public/newsfeed.png differ diff --git a/apps/desktop2/public/newsfeed@2x.png b/apps/desktop2/public/newsfeed@2x.png new file mode 100644 index 00000000..91ec29dc Binary files /dev/null and b/apps/desktop2/public/newsfeed@2x.png differ diff --git a/apps/desktop2/src/routes/__root.tsx b/apps/desktop2/src/routes/__root.tsx index f5b701c3..53312b17 100644 --- a/apps/desktop2/src/routes/__root.tsx +++ b/apps/desktop2/src/routes/__root.tsx @@ -25,6 +25,7 @@ interface RouterContext { // Profile accounts?: string[]; profile?: Metadata; + isNewUser?: boolean; // Editor initialValue?: EditorElement[]; } diff --git a/apps/desktop2/src/routes/auth/new/backup.tsx b/apps/desktop2/src/routes/auth/new/backup.tsx index 50c3545e..637e147a 100644 --- a/apps/desktop2/src/routes/auth/new/backup.tsx +++ b/apps/desktop2/src/routes/auth/new/backup.tsx @@ -1,5 +1,6 @@ import { CheckIcon } from "@lume/icons"; import type { AppRouteSearch } from "@lume/types"; +import { Spinner } from "@lume/ui"; import { displayNsec } from "@lume/utils"; import * as Checkbox from "@radix-ui/react-checkbox"; import { createFileRoute, useNavigate } from "@tanstack/react-router"; @@ -25,6 +26,7 @@ function Screen() { const [key, setKey] = useState(null); const [passphase, setPassphase] = useState(""); const [copied, setCopied] = useState(false); + const [loading, setLoading] = useState(false); const [confirm, setConfirm] = useState({ c1: false, c2: false, c3: false }); 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, password: passphase, + }).then((encrypted: string) => { + // update state + setKey(encrypted); + setLoading(false); }); - - setKey(encrypted); } catch (e) { + setLoading(false); toast.error(String(e)); } }; @@ -180,9 +188,10 @@ function Screen() { diff --git a/apps/desktop2/src/routes/auth/settings.tsx b/apps/desktop2/src/routes/auth/settings.tsx index 26b72cd9..66513a19 100644 --- a/apps/desktop2/src/routes/auth/settings.tsx +++ b/apps/desktop2/src/routes/auth/settings.tsx @@ -85,8 +85,11 @@ function Screen() { const eventId = await ark.set_settings(newSettings); if (eventId) { - console.log("event_id: ", eventId); - navigate({ to: "/$account/home", params: { account }, replace: true }); + return navigate({ + to: "/$account/home", + params: { account }, + replace: true, + }); } } catch (e) { setLoading(false); diff --git a/apps/desktop2/src/routes/newsfeed.tsx b/apps/desktop2/src/routes/newsfeed.tsx index b4c781fb..e58b8dfc 100644 --- a/apps/desktop2/src/routes/newsfeed.tsx +++ b/apps/desktop2/src/routes/newsfeed.tsx @@ -6,7 +6,8 @@ import { ArrowRightCircleIcon, ArrowRightIcon } from "@lume/icons"; import { type ColumnRouteSearch, type Event, Kind } from "@lume/types"; import { Spinner } from "@lume/ui"; 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"; export const Route = createFileRoute("/newsfeed")({ @@ -121,8 +122,6 @@ export function Screen() { } function Empty() { - const search = Route.useSearch(); - return (
@@ -135,29 +134,19 @@ function Empty() {

- - Show global newsfeed - - - Show trending notes + Show trending notes - Discover trending users + Discover trending users
diff --git a/apps/desktop2/src/routes/onboarding.tsx b/apps/desktop2/src/routes/onboarding.tsx new file mode 100644 index 00000000..8cb91667 --- /dev/null +++ b/apps/desktop2/src/routes/onboarding.tsx @@ -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): 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 ( +
+
+

Welcome to Lume

+

+ Here are a few suggestions to help you get started. +

+
+
+
+ background +
+
+
+ +
+

Mide

+
+ 👋 Yo! I'm Mide, and I'll be your friendly guide to Nostr and + beyond. Looking forward to our adventure together! +
+
+
+
+ +
+

You

+
+ How can I get started? +
+ + +
+
+ {userType === "veteran" ? ( +
+ +
+

Mide

+
+ 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! +
+
+
+ ) : null} + {userType === "veteran" ? ( +
+ +
+

You

+
+ Thanks! But I already know about Lume. +
+ +
+
+ ) : null} + {userType === "veteran" ? ( +
+ +
+

Mide

+
+ 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. +
+
+
+ ) : null} + {userType === "veteran" ? ( +
+ +
+

Mide

+
+ 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. +
+
+ If you're confused about the term "Column," you can imagine it + as mini-apps, with each column providing its own experience. +
+
+ Here is a quick guide for how to add a new column: +
+
+ +
+
+
+ ) : null} + {userType === "veteran" ? ( +
+ +
+

You

+
+ Can you introduce me to the UI? I am still confused. +
+
+
+ ) : null} + {userType === "veteran" ? ( +
+ +
+

Mide

+
+ Of course, here is a quick introduction video for Lume. +
+
+ +
+
+
+ ) : null} + {userType === "new" ? ( +
+ +
+

Mide

+
+ 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. +
+ + +
+
+ ) : null} + {userType === "new" ? ( +
+ +
+

You

+
+ My girlfriend introduced Nostr to me, and I have her NPUB. Can + I get the same experiences as her? +
+
+
+ ) : null} + {userType === "new" ? ( +
+ +
+

Mide

+
+ 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! +
+
+ +
+ +
+
+
+
+ ) : null} + {userType ? ( + <> +
+ +
+

You

+
+ Thank you. I can use Lume and explore Nostr by myself from + now on. +
+
+
+
+ +
+

Mide

+
+ 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: +
+ + [Website] nostr.org + + + + [Video] What is Nostr? + + + + [Develop] Github + + + + [Ecosystem] nostrapps.com + + +
+
+
+ +
+

Mide

+
+ If you want to close this onboarding board, you can click + the button below. +
+ +
+
+ + ) : null} +
+
+
+ ); +} + +function Mide() { + return ( + Ai-chan + ); +} + +function CurrentUser() { + const { account } = Route.useSearch(); + + return ( + + + + + + ); +} diff --git a/apps/desktop2/src/routes/store.official.tsx b/apps/desktop2/src/routes/store.official.tsx index 85feaa8d..d257a7b9 100644 --- a/apps/desktop2/src/routes/store.official.tsx +++ b/apps/desktop2/src/routes/store.official.tsx @@ -5,7 +5,6 @@ import { getCurrent } from "@tauri-apps/api/window"; import { readTextFile } from "@tauri-apps/plugin-fs"; export const Route = createFileRoute("/store/official")({ - component: Screen, beforeLoad: async () => { const resourcePath = await resolveResource( "resources/official_columns.json", @@ -18,6 +17,7 @@ export const Route = createFileRoute("/store/official")({ officialColumns, }; }, + component: Screen, }); function Screen() { diff --git a/package.json b/package.json index 1e72dd87..d883ef8e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "tauri": "tauri" }, "devDependencies": { - "@biomejs/biome": "^1.7.1", + "@biomejs/biome": "1.7.3", "@tauri-apps/cli": "2.0.0-beta.12", "turbo": "^1.13.3" }, diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index 44191b09..d8dc984d 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -1,10 +1,10 @@ import { - Kind, type Contact, type Event, type EventWithReplies, type Interests, type Keys, + Kind, type LumeColumn, type Metadata, type Settings, @@ -194,8 +194,10 @@ export class Ark { .filter((el) => el[0] === "e") ?.map((item) => item[1]); - if (eventIds && eventIds.length) { - eventIds.forEach((id) => seenIds.add(id)); + if (eventIds?.length) { + for (const id of eventIds) { + seenIds.add(id); + } } } diff --git a/packages/tailwindcss/package.json b/packages/tailwindcss/package.json index 3d2c2bde..99b41224 100644 --- a/packages/tailwindcss/package.json +++ b/packages/tailwindcss/package.json @@ -13,6 +13,7 @@ "@evilmartians/harmony": "^1.2.0", "@tailwindcss/forms": "^0.5.7", "@tailwindcss/typography": "^0.5.13", + "tailwind-gradient-mask-image": "^1.2.0", "tailwind-scrollbar": "^3.1.0", "tailwindcss": "^3.4.3" } diff --git a/packages/ui/package.json b/packages/ui/package.json index 24a660da..094757b5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -38,7 +38,6 @@ "slate-react": "^0.102.0", "sonner": "^1.4.41", "string-strip-html": "^13.4.8", - "tailwind-gradient-mask-image": "^1.2.0", "uqr": "^0.1.2", "use-debounce": "^10.0.0", "virtua": "^0.30.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a74365d..a9874cf8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,8 +46,8 @@ importers: version: 2.0.0-beta.3 devDependencies: '@biomejs/biome': - specifier: ^1.7.1 - version: 1.7.1 + specifier: 1.7.3 + version: 1.7.3 '@tauri-apps/cli': specifier: 2.0.0-beta.12 version: 2.0.0-beta.12 @@ -283,6 +283,9 @@ importers: '@tailwindcss/typography': specifier: ^0.5.13 version: 0.5.13(tailwindcss@3.4.3) + tailwind-gradient-mask-image: + specifier: ^1.2.0 + version: 1.2.0 tailwind-scrollbar: specifier: ^3.1.0 version: 3.1.0(tailwindcss@3.4.3) @@ -402,9 +405,6 @@ importers: string-strip-html: specifier: ^13.4.8 version: 13.4.8 - tailwind-gradient-mask-image: - specifier: ^1.2.0 - version: 1.2.0 uqr: specifier: ^0.1.2 version: 0.1.2 @@ -921,24 +921,24 @@ packages: '@babel/helper-validator-identifier': 7.24.5 to-fast-properties: 2.0.0 - /@biomejs/biome@1.7.1: - resolution: {integrity: sha512-wb2UNoFXcgaMdKXKT5ytsYntaogl2FSTjDt20CZynF3v7OXQUcIpTrr+be3XoOGpoZRj3Ytq9TSpmplUREXmeA==} + /@biomejs/biome@1.7.3: + resolution: {integrity: sha512-ogFQI+fpXftr+tiahA6bIXwZ7CSikygASdqMtH07J2cUzrpjyTMVc9Y97v23c7/tL1xCZhM+W9k4hYIBm7Q6cQ==} engines: {node: '>=14.21.3'} hasBin: true requiresBuild: true optionalDependencies: - '@biomejs/cli-darwin-arm64': 1.7.1 - '@biomejs/cli-darwin-x64': 1.7.1 - '@biomejs/cli-linux-arm64': 1.7.1 - '@biomejs/cli-linux-arm64-musl': 1.7.1 - '@biomejs/cli-linux-x64': 1.7.1 - '@biomejs/cli-linux-x64-musl': 1.7.1 - '@biomejs/cli-win32-arm64': 1.7.1 - '@biomejs/cli-win32-x64': 1.7.1 + '@biomejs/cli-darwin-arm64': 1.7.3 + '@biomejs/cli-darwin-x64': 1.7.3 + '@biomejs/cli-linux-arm64': 1.7.3 + '@biomejs/cli-linux-arm64-musl': 1.7.3 + '@biomejs/cli-linux-x64': 1.7.3 + '@biomejs/cli-linux-x64-musl': 1.7.3 + '@biomejs/cli-win32-arm64': 1.7.3 + '@biomejs/cli-win32-x64': 1.7.3 dev: true - /@biomejs/cli-darwin-arm64@1.7.1: - resolution: {integrity: sha512-qfLrIIB58dkgiY/1tgG6fSCBK22PZaSIf6blweZBsG6iMij05mEuJt50ne+zPnNFNUmt8t43NC/qOXT3iFHQBA==} + /@biomejs/cli-darwin-arm64@1.7.3: + resolution: {integrity: sha512-eDvLQWmGRqrPIRY7AIrkPHkQ3visEItJKkPYSHCscSDdGvKzYjmBJwG1Gu8+QC5ed6R7eiU63LEC0APFBobmfQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] @@ -946,8 +946,8 @@ packages: dev: true optional: true - /@biomejs/cli-darwin-x64@1.7.1: - resolution: {integrity: sha512-OGeyNsEcp5VnKbF9/TBjPCTHNEOm7oHegEve07U3KZmzqfpw2Oe3i9DVW8t6vvj1TYbrwWYCld25H34kBDY7Vg==} + /@biomejs/cli-darwin-x64@1.7.3: + resolution: {integrity: sha512-JXCaIseKRER7dIURsVlAJacnm8SG5I0RpxZ4ya3dudASYUc68WGl4+FEN03ABY3KMIq7hcK1tzsJiWlmXyosZg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] @@ -955,8 +955,8 @@ packages: dev: true optional: true - /@biomejs/cli-linux-arm64-musl@1.7.1: - resolution: {integrity: sha512-giH0/CzLOJ+wbxLxd5Shnr5xQf5fGnTRWLDe3lzjaF7IplVydNCEeZJtncB01SvyA6DAFJsvQ4LNxzAOQfEVCg==} + /@biomejs/cli-linux-arm64-musl@1.7.3: + resolution: {integrity: sha512-c8AlO45PNFZ1BYcwaKzdt46kYbuP6xPGuGQ6h4j3XiEDpyseRRUy/h+6gxj07XovmyxKnSX9GSZ6nVbZvcVUAw==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] @@ -964,8 +964,8 @@ packages: dev: true optional: true - /@biomejs/cli-linux-arm64@1.7.1: - resolution: {integrity: sha512-MQDf5wErj1iBvlcxCyOa0XqZYN8WJrupVgbNnqhntO3yVATg8GxduVUn1fDSaolznkDRsj7Pz3Xu1esBFwvfmg==} + /@biomejs/cli-linux-arm64@1.7.3: + resolution: {integrity: sha512-phNTBpo7joDFastnmZsFjYcDYobLTx4qR4oPvc9tJ486Bd1SfEVPHEvJdNJrMwUQK56T+TRClOQd/8X1nnjA9w==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] @@ -973,8 +973,8 @@ packages: dev: true optional: true - /@biomejs/cli-linux-x64-musl@1.7.1: - resolution: {integrity: sha512-ySNDtPhsLxU125IFHHAxfpoHBpkM56s4mEXeO70GZtgZay/o1h8IUPWCWf5Z7gKgc4jwgYN1U1U9xabI3hZVAg==} + /@biomejs/cli-linux-x64-musl@1.7.3: + resolution: {integrity: sha512-UdEHKtYGWEX3eDmVWvQeT+z05T9/Sdt2+F/7zmMOFQ7boANeX8pcO6EkJPK3wxMudrApsNEKT26rzqK6sZRTRA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] @@ -982,8 +982,8 @@ packages: dev: true optional: true - /@biomejs/cli-linux-x64@1.7.1: - resolution: {integrity: sha512-3wmCsGcC3KZ4pfTknXHfyMMlXPMhgfXVAcG5GlrR+Tq2JGiAw0EUydaLpsSBEbcG7IxH6OiUZEJZ95kAycCHBA==} + /@biomejs/cli-linux-x64@1.7.3: + resolution: {integrity: sha512-vnedYcd5p4keT3iD48oSKjOIRPYcjSNNbd8MO1bKo9ajg3GwQXZLAH+0Cvlr+eMsO67/HddWmscSQwTFrC/uPA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] @@ -991,8 +991,8 @@ packages: dev: true optional: true - /@biomejs/cli-win32-arm64@1.7.1: - resolution: {integrity: sha512-8hIDakEqZn0i6+388noYKdZ0ZrovTwnvMU/Qp/oJou0G7EPVdXupOe0oxiQSdRN0W7f6CS/yjPCYuVGzDG6r0g==} + /@biomejs/cli-win32-arm64@1.7.3: + resolution: {integrity: sha512-unNCDqUKjujYkkSxs7gFIfdasttbDC4+z0kYmcqzRk6yWVoQBL4dNLcCbdnJS+qvVDNdI9rHp2NwpQ0WAdla4Q==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] @@ -1000,8 +1000,8 @@ packages: dev: true optional: true - /@biomejs/cli-win32-x64@1.7.1: - resolution: {integrity: sha512-3W9k3uH6Ea6VOpAS9xkkAlS0LTfnGQjmIUCegZ8SDtK2NgJ1gO+qdEkGJb0ltahusFTN1QxJ107dM7ASA9IUEg==} + /@biomejs/cli-win32-x64@1.7.3: + resolution: {integrity: sha512-ZmByhbrnmz/UUFYB622CECwhKIPjJLLPr5zr3edhu04LzbfcOrz16VYeNq5dpO1ADG70FORhAJkaIGdaVBG00w==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -5912,7 +5912,7 @@ packages: /tailwind-gradient-mask-image@1.2.0: resolution: {integrity: sha512-tUJaGhvqbJFiVKJu6EU5n//KvGdVvY3L3VOFNqjztk13+ifAk00pcSNHBTgHfUiBGOEzDn0gFRbSmsftUV1lXA==} - dev: false + dev: true /tailwind-merge@2.3.0: resolution: {integrity: sha512-vkYrLpIP+lgR0tQCG6AP7zZXCTLc1Lnv/CCRT3BqJ9CZ3ui2++GPaGb1x/ILsINIMSYqqvrpqjUFsMNLlW99EA==} diff --git a/src-tauri/resources/official_columns.json b/src-tauri/resources/official_columns.json index 363cbf57..3c783373 100644 --- a/src-tauri/resources/official_columns.json +++ b/src-tauri/resources/official_columns.json @@ -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", "name": "For you", diff --git a/src-tauri/resources/system_columns.json b/src-tauri/resources/system_columns.json index 04a28622..032e9c35 100644 --- a/src-tauri/resources/system_columns.json +++ b/src-tauri/resources/system_columns.json @@ -1,4 +1,4 @@ [ - { "label": "home", "name": "Home", "content": "/newsfeed" }, - { "label": "open", "name": "Open", "content": "/open" } + { "label": "onboarding", "name": "Onboarding", "content": "/onboarding" }, + { "label": "open", "name": "Open", "content": "/open" } ] diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 7537439a..b204783f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -121,6 +121,7 @@ fn main() { nostr::metadata::get_balance, nostr::metadata::zap_profile, nostr::metadata::zap_event, + nostr::metadata::friend_to_friend, nostr::event::get_event, nostr::event::get_events_from, nostr::event::get_events, diff --git a/src-tauri/src/nostr/metadata.rs b/src-tauri/src/nostr/metadata.rs index 94bc7168..0dae4e49 100644 --- a/src-tauri/src/nostr/metadata.rs +++ b/src-tauri/src/nostr/metadata.rs @@ -82,6 +82,45 @@ pub async fn get_activities( } } +#[tauri::command] +pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result { + let client = &state.client; + + match PublicKey::from_bech32(npub) { + Ok(author) => { + let mut contact_list: Vec = 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] pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result { let client = &state.client;