feat: only query from local database and other improvements

This commit is contained in:
2024-10-09 09:15:49 +07:00
parent c40762cc04
commit 106c627ec4
11 changed files with 508 additions and 397 deletions

View File

@@ -182,7 +182,7 @@ async getGroup(id: string) : Promise<Result<string, string>> {
else return { status: "error", error: e as any };
}
},
async getAllGroups() : Promise<Result<string[], string>> {
async getAllGroups() : Promise<Result<RichEvent[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_all_groups") };
} catch (e) {
@@ -206,7 +206,7 @@ async getInterest(id: string) : Promise<Result<string, string>> {
else return { status: "error", error: e as any };
}
},
async getAllInterests() : Promise<Result<string[], string>> {
async getAllInterests() : Promise<Result<RichEvent[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_all_interests") };
} catch (e) {
@@ -398,9 +398,9 @@ async requestDelete(id: string) : Promise<Result<null, string>> {
else return { status: "error", error: e as any };
}
},
async search(query: string, until: string | null) : Promise<Result<RichEvent[], string>> {
async search(query: string) : Promise<Result<RichEvent[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("search", { query, until }) };
return { status: "ok", data: await TAURI_INVOKE("search", { query }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };

View File

@@ -2,7 +2,11 @@ import { replyTime } from "@/commons";
import { Note, Spinner } from "@/components";
import { User } from "@/components/user";
import { LumeWindow, useEvent } from "@/system";
import { memo } from "react";
import { nip19 } from "nostr-tools";
import { type ReactNode, memo, useMemo } from "react";
import reactStringReplace from "react-string-replace";
import { Hashtag } from "./hashtag";
import { MentionUser } from "./user";
export const MentionNote = memo(function MentionNote({
eventId,
@@ -36,9 +40,11 @@ export const MentionNote = memo(function MentionNote({
/>
</User.Root>
<div className="pl-2 inline select-text text-balance content-break overflow-hidden">
{event.content.length > 120
? `${event.content.substring(0, 120)}...`
: event.content}
{event.content.length > 300 ? (
`${event.content.substring(0, 300)}...`
) : (
<Content text={event.content} className="inline" />
)}
</div>
</div>
<div className="flex-1 flex items-center justify-between">
@@ -66,3 +72,64 @@ export const MentionNote = memo(function MentionNote({
</div>
);
});
function Content({ text, className }: { text: string; className?: string }) {
const content = useMemo(() => {
let replacedText: ReactNode[] | string = text.trim();
const nostr = replacedText
.split(/\s+/)
.filter((w) => w.startsWith("nostr:"));
replacedText = reactStringReplace(text, /(https?:\/\/\S+)/g, (match, i) => (
<a
key={match + i}
href={match}
target="_blank"
rel="noreferrer"
className="text-blue-600 dark:text-blue-400 !underline"
>
{match}
</a>
));
replacedText = reactStringReplace(replacedText, /#(\w+)/g, (match, i) => (
<Hashtag key={match + i} tag={match} />
));
for (const word of nostr) {
const bech32 = word.replace("nostr:", "");
const data = nip19.decode(bech32);
switch (data.type) {
case "npub":
replacedText = reactStringReplace(replacedText, word, (match, i) => (
<MentionUser key={match + i} pubkey={data.data} />
));
break;
case "nprofile":
replacedText = reactStringReplace(replacedText, word, (match, i) => (
<MentionUser key={match + i} pubkey={data.data.pubkey} />
));
break;
default:
replacedText = reactStringReplace(replacedText, word, (match, i) => (
<a
key={match + i}
href={`https://njump.me/${bech32}`}
target="_blank"
rel="noreferrer"
className="text-blue-600 dark:text-blue-400 !underline"
>
{match}
</a>
));
break;
}
}
return replacedText;
}, [text]);
return <div className={className}>{content}</div>;
}

View File

@@ -1,4 +1,5 @@
import { commands } from "@/commands.gen";
import { toLumeEvents } from "@/commons";
import { Spinner, User } from "@/components";
import { LumeWindow } from "@/system";
import type { LumeColumn, NostrEvent } from "@/types";
@@ -8,6 +9,7 @@ import { useQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { resolveResource } from "@tauri-apps/api/path";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { nanoid } from "nanoid";
import { useCallback } from "react";
export const Route = createLazyFileRoute("/columns/_layout/launchpad")({
@@ -101,7 +103,7 @@ function MyGroups() {
const res = await commands.getAllGroups();
if (res.status === "ok") {
const data = res.data.map((item) => JSON.parse(item) as NostrEvent);
const data = toLumeEvents(res.data);
return data;
} else {
throw new Error(res.error);
@@ -118,6 +120,7 @@ function MyGroups() {
(item: NostrEvent) => {
const name =
item.tags.find((tag) => tag[0] === "title")?.[1] || "Unnamed";
const label = item.tags.find((tag) => tag[0] === "d")?.[1] || nanoid();
return (
<div
@@ -144,7 +147,7 @@ function MyGroups() {
type="button"
onClick={() =>
LumeWindow.openColumn({
label: name,
label,
name,
url: `/columns/groups/${item.id}`,
})
@@ -211,7 +214,7 @@ function MyInterests() {
const res = await commands.getAllInterests();
if (res.status === "ok") {
const data = res.data.map((item) => JSON.parse(item) as NostrEvent);
const data = toLumeEvents(res.data);
return data;
} else {
throw new Error(res.error);
@@ -228,6 +231,8 @@ function MyInterests() {
(item: NostrEvent) => {
const name =
item.tags.find((tag) => tag[0] === "title")?.[1] || "Unnamed";
const label =
item.tags.find((tag) => tag[0] === "label")?.[1] || nanoid();
return (
<div
@@ -250,7 +255,7 @@ function MyInterests() {
type="button"
onClick={() =>
LumeWindow.openColumn({
label: name,
label,
name,
url: `/columns/interests/${item.id}`,
})

View File

@@ -27,12 +27,18 @@ function Screen() {
switch (event.kind) {
case Kind.Text:
return <TextNote key={event.id} event={event} className="mb-3" />;
return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
case Kind.Metadata:
return (
<div
key={event.id}
className="p-3 mb-3 bg-white dark:bg-black/20 rounded-xl shadow-primary dark:ring-1 dark:ring-white/5"
className="p-3 border-b-[.5px] border-neutral-300 dark:border-neutral-700"
>
<User.Provider pubkey={event.pubkey}>
<User.Root className="flex flex-col w-full h-full gap-2">
@@ -61,7 +67,13 @@ function Screen() {
</div>
);
default:
return <TextNote key={event.id} event={event} className="mb-3" />;
return (
<TextNote
key={event.id}
event={event}
className="border-b-[.5px] border-neutral-300 dark:border-neutral-700"
/>
);
}
},
[events],
@@ -71,7 +83,7 @@ function Screen() {
startTransition(async () => {
if (!query.length) return;
const res = await commands.search(query, null);
const res = await commands.search(query);
if (res.status === "ok") {
const data = toLumeEvents(res.data);
@@ -114,7 +126,7 @@ function Screen() {
scrollHideDelay={300}
className="overflow-hidden size-full flex-1"
>
<ScrollArea.Viewport ref={ref} className="relative h-full px-3">
<ScrollArea.Viewport ref={ref} className="relative h-full">
<Virtualizer scrollRef={ref}>
{isPending ? (
<div className="w-full h-[200px] flex gap-2 items-center justify-center">

View File

@@ -1,12 +1,17 @@
import { commands } from "@/commands.gen";
import { replyTime, toLumeEvents } from "@/commons";
import { Note, Spinner, User } from "@/components";
import { Hashtag } from "@/components/note/mentions/hashtag";
import { MentionUser } from "@/components/note/mentions/user";
import { type LumeEvent, LumeWindow } from "@/system";
import { ColumnsPlusLeft } from "@phosphor-icons/react";
import { Kind } from "@/types";
import { ArrowRight } from "@phosphor-icons/react";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
import { memo, useRef } from "react";
import { nip19 } from "nostr-tools";
import { type ReactNode, memo, useMemo, useRef } from "react";
import reactStringReplace from "react-string-replace";
import { Virtualizer } from "virtua";
export const Route = createLazyFileRoute("/columns/_layout/stories")({
@@ -14,7 +19,7 @@ export const Route = createLazyFileRoute("/columns/_layout/stories")({
});
function Screen() {
const { contacts } = Route.useRouteContext();
const contacts = Route.useLoaderData();
const ref = useRef<HTMLDivElement>(null);
return (
@@ -25,9 +30,15 @@ function Screen() {
>
<ScrollArea.Viewport ref={ref} className="relative h-full px-3 pb-3">
<Virtualizer scrollRef={ref} overscan={0}>
{contacts.map((contact) => (
<StoryItem key={contact} contact={contact} />
))}
{!contacts ? (
<div className="w-full h-24 flex items-center justify-center">
<Spinner className="size-4" />
</div>
) : (
contacts.map((contact) => (
<StoryItem key={contact} contact={contact} />
))
)}
</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
@@ -59,6 +70,7 @@ function StoryItem({ contact }: { contact: string }) {
throw new Error(res.error);
}
},
select: (data) => data.filter((ev) => ev.kind === Kind.Text),
refetchOnWindowFocus: false,
});
@@ -77,9 +89,10 @@ function StoryItem({ contact }: { contact: string }) {
<button
type="button"
onClick={() => LumeWindow.openProfile(contact)}
className="size-7 inline-flex items-center justify-center rounded-lg text-neutral-500 hover:bg-neutral-100 dark:hover:bg-white/20"
className="h-7 w-max px-2.5 inline-flex gap-1 items-center justify-center rounded-full text-sm font-medium hover:bg-neutral-100 dark:hover:bg-white/20"
>
<ColumnsPlusLeft className="size-4" />
Open
<ArrowRight className="size-3" weight="bold" />
</button>
</div>
</div>
@@ -129,9 +142,10 @@ const StoryEvent = memo(function StoryEvent({ event }: { event: LumeEvent }) {
className="shrink-0 inline font-medium text-blue-500"
suffix=":"
/>
<div className="pl-2 inline select-text text-balance content-break overflow-hidden">
{event.content}
</div>
<Content
text={event.content}
className="pl-2 inline select-text text-balance content-break overflow-hidden"
/>
</div>
<div className="flex-1 flex items-center justify-between">
<span className="text-sm text-neutral-500">
@@ -148,3 +162,64 @@ const StoryEvent = memo(function StoryEvent({ event }: { event: LumeEvent }) {
</Note.Provider>
);
});
function Content({ text, className }: { text: string; className?: string }) {
const content = useMemo(() => {
let replacedText: ReactNode[] | string = text.trim();
const nostr = replacedText
.split(/\s+/)
.filter((w) => w.startsWith("nostr:"));
replacedText = reactStringReplace(text, /(https?:\/\/\S+)/g, (match, i) => (
<a
key={match + i}
href={match}
target="_blank"
rel="noreferrer"
className="text-blue-600 dark:text-blue-400 !underline"
>
{match}
</a>
));
replacedText = reactStringReplace(replacedText, /#(\w+)/g, (match, i) => (
<Hashtag key={match + i} tag={match} />
));
for (const word of nostr) {
const bech32 = word.replace("nostr:", "");
const data = nip19.decode(bech32);
switch (data.type) {
case "npub":
replacedText = reactStringReplace(replacedText, word, (match, i) => (
<MentionUser key={match + i} pubkey={data.data} />
));
break;
case "nprofile":
replacedText = reactStringReplace(replacedText, word, (match, i) => (
<MentionUser key={match + i} pubkey={data.data.pubkey} />
));
break;
default:
replacedText = reactStringReplace(replacedText, word, (match, i) => (
<a
key={match + i}
href={`https://njump.me/${bech32}`}
target="_blank"
rel="noreferrer"
className="text-blue-600 dark:text-blue-400 !underline"
>
{match}
</a>
));
break;
}
}
return replacedText;
}, [text]);
return <div className={className}>{content}</div>;
}

View File

@@ -2,12 +2,11 @@ import { commands } from "@/commands.gen";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/columns/_layout/stories")({
beforeLoad: async () => {
loader: async () => {
const res = await commands.getContactList();
if (res.status === "ok") {
const contacts = res.data;
return { contacts };
return res.data;
} else {
throw new Error(res.error);
}