feat: update onboarding flow
This commit is contained in:
@@ -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 <NIP89 className={className} />;
|
||||
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) => <ImagePreview key={match + i} url={match} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (videos.length) {
|
||||
for (const video of videos) {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
video,
|
||||
(match, i) => <VideoPreview key={match + i} url={match} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (audios.length) {
|
||||
for (const audio of audios) {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
audio,
|
||||
(match, i) => <VideoPreview key={match + i} url={match} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (hashtags.length) {
|
||||
for (const hashtag of hashtags) {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
hashtag,
|
||||
(match, i) => {
|
||||
if (storage.settings.hashtag)
|
||||
return <Hashtag key={match + i} tag={hashtag} />;
|
||||
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) => (
|
||||
<MentionNote key={match + i} eventId={decoded.data} />
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (decoded.type === "nevent") {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
event,
|
||||
(match, i) => (
|
||||
<MentionNote key={match + i} eventId={decoded.data.id} />
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mentions.length) {
|
||||
for (const mention of mentions) {
|
||||
const address = mention
|
||||
.replace("nostr:", "")
|
||||
.replace("@", "")
|
||||
.replace(/[^a-zA-Z0-9]/g, "");
|
||||
const decoded = nip19.decode(address);
|
||||
|
||||
if (decoded.type === "npub") {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
mention,
|
||||
(match, i) => (
|
||||
<MentionUser key={match + i} pubkey={decoded.data} />
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (decoded.type === "nprofile" || decoded.type === "naddr") {
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
mention,
|
||||
(match, i) => (
|
||||
<MentionUser key={match + i} pubkey={decoded.data.pubkey} />
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsedContent = reactStringReplace(
|
||||
parsedContent,
|
||||
/(https?:\/\/\S+)/g,
|
||||
(match, i) => {
|
||||
const url = new URL(match);
|
||||
|
||||
if (!linkPreview) {
|
||||
linkPreview = match;
|
||||
return <LinkPreview key={match + i} url={url.toString()} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={match + i}
|
||||
to={url.toString()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="break-all font-normal text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
{url.toString()}
|
||||
</Link>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
parsedContent = reactStringReplace(parsedContent, "\n", () => {
|
||||
return <div key={nanoid()} className="h-3" />;
|
||||
});
|
||||
|
||||
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 <NIP89 className={className} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"break-p select-text whitespace-pre-line text-balance leading-normal",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{parsedContent}
|
||||
<div className={cn("", className)}>
|
||||
<div className="break-p select-text whitespace-pre-line text-balance leading-normal">
|
||||
{richContent}
|
||||
</div>
|
||||
{storage.settings.translation ? (
|
||||
translated ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
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
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={translate}
|
||||
className="mt-2 text-sm text-blue-500 hover:text-blue-600 border-none shadow-none focus:outline-none"
|
||||
>
|
||||
Translate to Vietnamese
|
||||
</button>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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<IColumn> = 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],
|
||||
|
||||
50
packages/types/index.d.ts
vendored
50
packages/types/index.d.ts
vendored
@@ -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<Resource>;
|
||||
}
|
||||
|
||||
export interface NDKCacheUser {
|
||||
pubkey: string;
|
||||
profile: string | NDKUserProfile;
|
||||
|
||||
Reference in New Issue
Block a user