From e0d4c53098b7ae6d98d99e2909f0d2d3c0aa0d58 Mon Sep 17 00:00:00 2001
From: reya
Date: Fri, 12 Jan 2024 10:12:06 +0700
Subject: [PATCH] feat: update onboarding flow
---
apps/desktop/src/routes/auth/create.tsx | 4 +-
apps/desktop/src/routes/auth/login-key.tsx | 4 +-
.../src/routes/auth/login-nsecbunker.tsx | 4 +-
apps/desktop/src/routes/auth/login-oauth.tsx | 51 ++-
apps/desktop/src/routes/auth/onboarding.tsx | 109 ++++--
packages/ark/src/components/note/content.tsx | 320 +++++++++++++++++-
packages/ark/src/hooks/useRichContent.tsx | 6 +-
packages/lume-column-timeline/src/home.tsx | 14 +-
packages/storage/index.ts | 21 +-
packages/types/index.d.ts | 50 ---
10 files changed, 468 insertions(+), 115 deletions(-)
diff --git a/apps/desktop/src/routes/auth/create.tsx b/apps/desktop/src/routes/auth/create.tsx
index ef75026e..027d24a4 100644
--- a/apps/desktop/src/routes/auth/create.tsx
+++ b/apps/desktop/src/routes/auth/create.tsx
@@ -85,7 +85,7 @@ export function CreateAccountScreen() {
setOnboarding(true);
- return navigate("/auth/onboarding");
+ return navigate("/auth/onboarding", { replace: true });
};
const onSubmit = async (data: { username: string; email: string }) => {
@@ -164,7 +164,7 @@ export function CreateAccountScreen() {
setOnboarding(true);
setIsLoading(false);
- return navigate("/auth/onboarding");
+ return navigate("/auth/onboarding", { replace: true });
} catch (e) {
setIsLoading(false);
toast.error(String(e));
diff --git a/apps/desktop/src/routes/auth/login-key.tsx b/apps/desktop/src/routes/auth/login-key.tsx
index 22f455c5..34c50684 100644
--- a/apps/desktop/src/routes/auth/login-key.tsx
+++ b/apps/desktop/src/routes/auth/login-key.tsx
@@ -35,7 +35,7 @@ export function LoginWithKey() {
privkey: privkey,
});
- return navigate("/auth/onboarding");
+ return navigate("/auth/onboarding", { replace: true });
} catch (e) {
setLoading(false);
setError("nsec", {
@@ -98,7 +98,7 @@ export function LoginWithKey() {
-
-
toggleAutoupdate()}
- className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800"
- >
-
-
-
-
- Auto check for update on Login
-
-
- Keep Lume up to date with latest version, always have new
- features and bug free.
-
-
-
+
+
toggleLowPower()}
+ className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800"
+ >
+
+
+
+
Low Power Mode
+
+ Limited relay connection and hide all media, sustainable for low
+ network environment
+
+
+
+
+
toggleTranslation()}
+ className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full outline-none data-[state=checked]:bg-blue-500 bg-neutral-800"
+ >
+
+
+
+
+ Translation (nostr.wine)
+
+
+ Translate text to your preferred language, powered by Nostr Wine
+
+
+
+ {settings.translation ? (
+
+
Translate API Key
+ setAPIKey(e.target.value)}
+ className="w-full text-xl border-transparent outline-none focus:outline-none focus:ring-0 focus:border-none h-11 rounded-lg ring-0 placeholder:text-neutral-600 bg-neutral-900"
+ />
+
+ ) : null}
diff --git a/packages/ark/src/components/note/content.tsx b/packages/ark/src/components/note/content.tsx
index ddd18fcd..3d7bd754 100644
--- a/packages/ark/src/components/note/content.tsx
+++ b/packages/ark/src/components/note/content.tsx
@@ -1,26 +1,326 @@
import { cn } from "@lume/utils";
import { NDKKind } from "@nostr-dev-kit/ndk";
-import { useNoteContext, useRichContent } from "../..";
+import { fetch } from "@tauri-apps/plugin-http";
+import getUrls from "get-urls";
+import { nanoid } from "nanoid";
+import { nip19 } from "nostr-tools";
+import { ReactNode, useMemo, useState } from "react";
+import { Link } from "react-router-dom";
+import reactStringReplace from "react-string-replace";
+import {
+ Hashtag,
+ ImagePreview,
+ LinkPreview,
+ MentionNote,
+ MentionUser,
+ VideoPreview,
+ useNoteContext,
+ useStorage,
+} from "../..";
import { NIP89 } from "./nip89";
+const NOSTR_MENTIONS = [
+ "@npub1",
+ "nostr:npub1",
+ "nostr:nprofile1",
+ "nostr:naddr1",
+ "npub1",
+ "nprofile1",
+ "naddr1",
+ "Nostr:npub1",
+ "Nostr:nprofile1",
+ "Nostr:naddre1",
+];
+
+const NOSTR_EVENTS = [
+ "@nevent1",
+ "@note1",
+ "@nostr:note1",
+ "@nostr:nevent1",
+ "nostr:note1",
+ "note1",
+ "nostr:nevent1",
+ "nevent1",
+ "Nostr:note1",
+ "Nostr:nevent1",
+];
+
+// const BITCOINS = ['lnbc', 'bc1p', 'bc1q'];
+
+const IMAGES = ["jpg", "jpeg", "gif", "png", "webp", "avif", "tiff"];
+
+const VIDEOS = [
+ "mp4",
+ "mov",
+ "webm",
+ "wmv",
+ "flv",
+ "mts",
+ "avi",
+ "ogv",
+ "mkv",
+ "m3u8",
+];
+
+const AUDIOS = ["mp3", "ogg", "wav"];
+
export function NoteContent({
className,
}: {
className?: string;
}) {
+ const storage = useStorage();
const event = useNoteContext();
- const { parsedContent } = useRichContent(event.content);
- if (event.kind !== NDKKind.Text) return ;
+ const [content, setContent] = useState(event.content);
+ const [translated, setTranslated] = useState(false);
+
+ const richContent = useMemo(() => {
+ if (event.kind !== NDKKind.Text) return content;
+
+ let parsedContent: string | ReactNode[] = content.replace(/\n+/g, "\n");
+ let linkPreview: string;
+ let images: string[] = [];
+ let videos: string[] = [];
+ let audios: string[] = [];
+ let events: string[] = [];
+
+ const text = parsedContent;
+ const words = text.split(/( |\n)/);
+ const urls = [...getUrls(text)];
+
+ if (storage.settings.media && !storage.settings.lowPower) {
+ images = urls.filter((word) =>
+ IMAGES.some((el) => {
+ const url = new URL(word);
+ const extension = url.pathname.split(".")[1];
+ if (extension === el) return true;
+ return false;
+ }),
+ );
+ videos = urls.filter((word) =>
+ VIDEOS.some((el) => {
+ const url = new URL(word);
+ const extension = url.pathname.split(".")[1];
+ if (extension === el) return true;
+ return false;
+ }),
+ );
+ audios = urls.filter((word) =>
+ AUDIOS.some((el) => {
+ const url = new URL(word);
+ const extension = url.pathname.split(".")[1];
+ if (extension === el) return true;
+ return false;
+ }),
+ );
+ }
+
+ events = words.filter((word) =>
+ NOSTR_EVENTS.some((el) => word.startsWith(el)),
+ );
+
+ const hashtags = words.filter((word) => word.startsWith("#"));
+ const mentions = words.filter((word) =>
+ NOSTR_MENTIONS.some((el) => word.startsWith(el)),
+ );
+
+ try {
+ if (images.length) {
+ for (const image of images) {
+ parsedContent = reactStringReplace(
+ parsedContent,
+ image,
+ (match, i) => ,
+ );
+ }
+ }
+
+ if (videos.length) {
+ for (const video of videos) {
+ parsedContent = reactStringReplace(
+ parsedContent,
+ video,
+ (match, i) => ,
+ );
+ }
+ }
+
+ if (audios.length) {
+ for (const audio of audios) {
+ parsedContent = reactStringReplace(
+ parsedContent,
+ audio,
+ (match, i) => ,
+ );
+ }
+ }
+
+ if (hashtags.length) {
+ for (const hashtag of hashtags) {
+ parsedContent = reactStringReplace(
+ parsedContent,
+ hashtag,
+ (match, i) => {
+ if (storage.settings.hashtag)
+ return ;
+ return null;
+ },
+ );
+ }
+ }
+
+ if (events.length) {
+ for (const event of events) {
+ const address = event
+ .replace("nostr:", "")
+ .replace(/[^a-zA-Z0-9]/g, "");
+ const decoded = nip19.decode(address);
+
+ if (decoded.type === "note") {
+ parsedContent = reactStringReplace(
+ parsedContent,
+ event,
+ (match, i) => (
+
+ ),
+ );
+ }
+
+ if (decoded.type === "nevent") {
+ parsedContent = reactStringReplace(
+ parsedContent,
+ event,
+ (match, i) => (
+
+ ),
+ );
+ }
+ }
+ }
+
+ if (mentions.length) {
+ for (const mention of mentions) {
+ const address = mention
+ .replace("nostr:", "")
+ .replace("@", "")
+ .replace(/[^a-zA-Z0-9]/g, "");
+ const decoded = nip19.decode(address);
+
+ if (decoded.type === "npub") {
+ parsedContent = reactStringReplace(
+ parsedContent,
+ mention,
+ (match, i) => (
+
+ ),
+ );
+ }
+
+ if (decoded.type === "nprofile" || decoded.type === "naddr") {
+ parsedContent = reactStringReplace(
+ parsedContent,
+ mention,
+ (match, i) => (
+
+ ),
+ );
+ }
+ }
+ }
+
+ parsedContent = reactStringReplace(
+ parsedContent,
+ /(https?:\/\/\S+)/g,
+ (match, i) => {
+ const url = new URL(match);
+
+ if (!linkPreview) {
+ linkPreview = match;
+ return ;
+ }
+
+ return (
+
+ {url.toString()}
+
+ );
+ },
+ );
+
+ parsedContent = reactStringReplace(parsedContent, "\n", () => {
+ return
;
+ });
+
+ if (typeof parsedContent[0] === "string") {
+ parsedContent[0] = parsedContent[0].trimStart();
+ }
+
+ return parsedContent;
+ } catch (e) {
+ console.warn("[parser] parse failed: ", e);
+ return parsedContent;
+ }
+ }, [content]);
+
+ const translate = async () => {
+ try {
+ const res = await fetch("https://translate.nostr.wine/translate", {
+ method: "POST",
+ body: JSON.stringify({
+ q: content,
+ target: "vi",
+ api_key: storage.settings.translateApiKey,
+ }),
+ headers: { "Content-Type": "application/json" },
+ });
+
+ const data = await res.json();
+
+ setContent(data.translatedText);
+ setTranslated(true);
+ } catch (e) {
+ console.error(String(e));
+ }
+ };
+
+ if (event.kind !== NDKKind.Text) {
+ return
;
+ }
return (
-
- {parsedContent}
+
+
+ {richContent}
+
+ {storage.settings.translation ? (
+ translated ? (
+
{
+ setTranslated(false);
+ setContent(event.content);
+ }}
+ className="mt-2 text-sm text-blue-500 hover:text-blue-600 border-none shadow-none focus:outline-none"
+ >
+ Show original content
+
+ ) : (
+
+ Translate to Vietnamese
+
+ )
+ ) : null}
);
}
diff --git a/packages/ark/src/hooks/useRichContent.tsx b/packages/ark/src/hooks/useRichContent.tsx
index 7b67901b..bd829d9e 100644
--- a/packages/ark/src/hooks/useRichContent.tsx
+++ b/packages/ark/src/hooks/useRichContent.tsx
@@ -73,7 +73,7 @@ export function useRichContent(content: string) {
const words = text.split(/( |\n)/);
const urls = [...getUrls(text)];
- if (storage.settings.media && !storage.settings.lowPowerMode) {
+ if (storage.settings.media && !storage.settings.lowPower) {
images = urls.filter((word) =>
IMAGES.some((el) => {
const url = new URL(word);
@@ -238,9 +238,9 @@ export function useRichContent(content: string) {
parsedContent[0] = parsedContent[0].trimStart();
}
- return { parsedContent };
+ return parsedContent;
} catch (e) {
console.warn("[parser] parse failed: ", e);
- return { parsedContent };
+ return parsedContent;
}
}
diff --git a/packages/lume-column-timeline/src/home.tsx b/packages/lume-column-timeline/src/home.tsx
index ff510603..ee324e82 100644
--- a/packages/lume-column-timeline/src/home.tsx
+++ b/packages/lume-column-timeline/src/home.tsx
@@ -3,7 +3,7 @@ import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
import { EmptyFeed } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
-import { useInfiniteQuery } from "@tanstack/react-query";
+import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react";
import { CacheSnapshot, VList, VListHandle } from "virtua";
@@ -47,6 +47,18 @@ export function HomeRoute({ colKey }: { colKey: string }) {
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
+ initialData: () => {
+ const queryClient = useQueryClient();
+ const queryCacheData = queryClient.getQueryState([colKey])
+ ?.data as NDKEvent[];
+ if (queryCacheData) {
+ return {
+ pageParams: [undefined, 1],
+ pages: [queryCacheData],
+ };
+ }
+ },
+ staleTime: 120 * 1000,
refetchOnWindowFocus: false,
});
diff --git a/packages/storage/index.ts b/packages/storage/index.ts
index 5d41f66e..c0af9112 100644
--- a/packages/storage/index.ts
+++ b/packages/storage/index.ts
@@ -25,7 +25,9 @@ export class LumeStorage {
hashtag: boolean;
depot: boolean;
tunnelUrl: string;
- lowPowerMode: boolean;
+ lowPower: boolean;
+ translation: boolean;
+ translateApiKey: string;
};
constructor(db: Database, platform: Platform) {
@@ -38,7 +40,9 @@ export class LumeStorage {
hashtag: true,
depot: false,
tunnelUrl: "",
- lowPowerMode: false,
+ lowPower: false,
+ translation: false,
+ translateApiKey: "",
};
}
@@ -55,6 +59,12 @@ export class LumeStorage {
if (item.key === "media") this.settings.media = !!parseInt(item.value);
if (item.key === "depot") this.settings.depot = !!parseInt(item.value);
if (item.key === "tunnel_url") this.settings.tunnelUrl = item.value;
+ if (item.key === "lowPower")
+ this.settings.lowPower = !!parseInt(item.value);
+ if (item.key === "translation")
+ this.settings.translation = !!parseInt(item.value);
+ if (item.key === "translateApiKey")
+ this.settings.translateApiKey = item.value;
}
const account = await this.getActiveAccount();
@@ -320,10 +330,13 @@ export class LumeStorage {
}
public async getColumns() {
+ if (!this.account) return [];
+
const columns: Array
= await this.#db.select(
"SELECT * FROM columns WHERE account_id = $1 ORDER BY created_at DESC;",
[this.account.id],
);
+
return columns;
}
@@ -366,7 +379,9 @@ export class LumeStorage {
const currentSetting = await this.checkSettingValue(key);
if (!currentSetting) {
- this.settings[key] === !!parseInt(value);
+ if (key !== "translateApiKey" && key !== "tunnelUrl")
+ this.settings[key] === !!parseInt(value);
+
return await this.#db.execute(
"INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);",
[key, value],
diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts
index 23218114..fb74ba79 100644
--- a/packages/types/index.d.ts
+++ b/packages/types/index.d.ts
@@ -20,19 +20,6 @@ export interface Account {
relayList: string[];
}
-export interface WidgetGroup {
- title: string;
- data: WidgetGroupItem[];
-}
-
-export interface WidgetGroupItem {
- title: string;
- description: string;
- content: string;
- kind: number;
- icon?: string;
-}
-
export interface IColumn {
id?: number;
kind: number;
@@ -40,32 +27,6 @@ export interface IColumn {
content: string;
}
-export interface WidgetProps {
- id?: string;
- account_id?: number;
- kind: number;
- title: string;
- content: string;
-}
-
-export interface Chats {
- id: string;
- event_id?: string;
- receiver_pubkey: string;
- sender_pubkey: string;
- content: string;
- tags: string[][];
- created_at: number;
- new_messages?: number;
-}
-
-export interface Relays {
- id?: string;
- account_id?: number;
- relay: string;
- purpose?: string;
-}
-
export interface Opengraph {
url: string;
title?: string;
@@ -97,17 +58,6 @@ export interface NostrBuildResponse {
};
}
-export interface Resource {
- id: string;
- title: string;
- image: string;
-}
-
-export interface Resources {
- title: string;
- data: Array;
-}
-
export interface NDKCacheUser {
pubkey: string;
profile: string | NDKUserProfile;