From 26e26758ced2277b87018a29e3af16ed3bb10831 Mon Sep 17 00:00:00 2001 From: reya Date: Fri, 18 Oct 2024 07:59:51 +0700 Subject: [PATCH] update --- src-tauri/resources/columns.json | 2 +- src-tauri/src/commands/metadata.rs | 33 +- src/components/column.tsx | 51 +- src/routes/_layout.lazy.tsx | 20 +- .../columns/_layout/notification.$id.lazy.tsx | 595 +++++++++--------- src/system/window.ts | 7 +- src/types.ts | 5 +- 7 files changed, 376 insertions(+), 337 deletions(-) diff --git a/src-tauri/resources/columns.json b/src-tauri/resources/columns.json index bfebe7e5..9f2e3503 100644 --- a/src-tauri/resources/columns.json +++ b/src-tauri/resources/columns.json @@ -8,7 +8,7 @@ }, { "default": true, - "label": "Launchpad", + "label": "launchpad", "name": "Launchpad", "description": "Expand your experiences.", "url": "/columns/launchpad" diff --git a/src-tauri/src/commands/metadata.rs b/src-tauri/src/commands/metadata.rs index 8f4af983..646d4995 100644 --- a/src-tauri/src/commands/metadata.rs +++ b/src-tauri/src/commands/metadata.rs @@ -104,16 +104,35 @@ pub async fn set_contact_list( #[tauri::command] #[specta::specta] -pub fn get_contact_list(id: String, state: State<'_, Nostr>) -> Result, String> { - let contact_state = state.contact_list.lock().unwrap().clone(); +pub async fn get_contact_list(id: String, state: State<'_, Nostr>) -> Result, String> { + let client = &state.client; let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?; - match contact_state.get(&public_key) { - Some(contact_list) => { - let vec: Vec = contact_list.iter().map(|f| f.public_key.to_hex()).collect(); - Ok(vec) + let filter = Filter::new() + .author(public_key) + .kind(Kind::ContactList) + .limit(1); + + let mut contact_list: Vec = Vec::new(); + + match client.database().query(vec![filter]).await { + Ok(events) => { + if let Some(event) = events.into_iter().next() { + for tag in event.tags.into_iter() { + if let Some(TagStandard::PublicKey { + public_key, + uppercase: false, + .. + }) = tag.to_standardized() + { + contact_list.push(public_key.to_hex()) + } + } + } + + Ok(contact_list) } - None => Err("Contact list is empty.".into()), + Err(e) => Err(e.to_string()), } } diff --git a/src/components/column.tsx b/src/components/column.tsx index 997f9bf3..83601e1f 100644 --- a/src/components/column.tsx +++ b/src/components/column.tsx @@ -1,12 +1,11 @@ import { commands } from "@/commands.gen"; -import { appColumns } from "@/commons"; import { useRect } from "@/system"; import type { LumeColumn } from "@/types"; import { CaretDown, Check } from "@phosphor-icons/react"; -import { useStore } from "@tanstack/react-store"; import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { User } from "./user"; export function Column({ column }: { column: LumeColumn }) { const webviewLabel = useMemo(() => `column-${column.label}`, [column.label]); @@ -73,34 +72,30 @@ export function Column({ column }: { column: LumeColumn }) { return (
-
+
); } -function Header({ label }: { label: string }) { +function Header({ + label, + name, + account, +}: { label: string; name: string; account?: string }) { const [title, setTitle] = useState(""); const [isChanged, setIsChanged] = useState(false); - const column = useStore(appColumns, (state) => - state.find((col) => col.label === label), - ); - - const saveNewTitle = async () => { - await getCurrentWindow().emit("columns", { - type: "set_title", - label, - title, - }); - setIsChanged(false); - }; - const showContextMenu = useCallback(async (e: React.MouseEvent) => { e.preventDefault(); const window = getCurrentWindow(); + const menuItems = await Promise.all([ MenuItem.new({ text: "Reload", @@ -108,10 +103,6 @@ function Header({ label }: { label: string }) { await commands.reloadColumn(label); }, }), - MenuItem.new({ - text: "Open in new window", - action: () => console.log("not implemented."), - }), PredefinedMenuItem.new({ item: "Separator" }), MenuItem.new({ text: "Move left", @@ -152,6 +143,15 @@ function Header({ label }: { label: string }) { await menu.popup().catch((e) => console.error(e)); }, []); + const saveNewTitle = async () => { + await getCurrentWindow().emit("columns", { + type: "set_title", + label, + title, + }); + setIsChanged(false); + }; + useEffect(() => { if (title.length > 0) setIsChanged(true); }, [title.length]); @@ -160,13 +160,20 @@ function Header({ label }: { label: string }) {
+ {account?.length ? ( + + + + + + ) : null}
setTitle(e.currentTarget.textContent)} className="text-[12px] font-semibold focus:outline-none" > - {column.name} + {name}
{isChanged ? ( - +
) : null} -
+
); diff --git a/src/routes/columns/_layout/notification.$id.lazy.tsx b/src/routes/columns/_layout/notification.$id.lazy.tsx index 65ea2011..afa7a2db 100644 --- a/src/routes/columns/_layout/notification.$id.lazy.tsx +++ b/src/routes/columns/_layout/notification.$id.lazy.tsx @@ -1,331 +1,336 @@ -import { commands } from '@/commands.gen' -import { decodeZapInvoice, formatCreatedAt } from '@/commons' -import { Note, RepostIcon, Spinner, User } from '@/components' -import { LumeEvent, LumeWindow, useEvent } from '@/system' -import { Kind, type NostrEvent } from '@/types' -import { Info } from '@phosphor-icons/react' -import * as ScrollArea from '@radix-ui/react-scroll-area' -import * as Tabs from '@radix-ui/react-tabs' -import { useQuery } from '@tanstack/react-query' -import { createLazyFileRoute } from '@tanstack/react-router' -import { getCurrentWindow } from '@tauri-apps/api/window' -import { type ReactNode, useEffect, useMemo, useRef } from 'react' -import { Virtualizer } from 'virtua' +import { commands } from "@/commands.gen"; +import { decodeZapInvoice, formatCreatedAt } from "@/commons"; +import { Note, RepostIcon, Spinner, User } from "@/components"; +import { LumeEvent, LumeWindow, useEvent } from "@/system"; +import { Kind, type NostrEvent } from "@/types"; +import { Info } from "@phosphor-icons/react"; +import * as ScrollArea from "@radix-ui/react-scroll-area"; +import * as Tabs from "@radix-ui/react-tabs"; +import { useQuery } from "@tanstack/react-query"; +import { createLazyFileRoute } from "@tanstack/react-router"; +import { getCurrentWindow } from "@tauri-apps/api/window"; +import { nip19 } from "nostr-tools"; +import { type ReactNode, useEffect, useMemo, useRef } from "react"; +import { Virtualizer } from "virtua"; -export const Route = createLazyFileRoute('/columns/_layout/notification/$id')({ - component: Screen, -}) +export const Route = createLazyFileRoute("/columns/_layout/notification/$id")({ + component: Screen, +}); function Screen() { - const { queryClient } = Route.useRouteContext() - const { isLoading, data } = useQuery({ - queryKey: ['notification'], - queryFn: async () => { - const res = await commands.getNotifications() + const { id } = Route.useParams(); + const { queryClient } = Route.useRouteContext(); + const { isLoading, data } = useQuery({ + queryKey: ["notification", id], + queryFn: async () => { + const res = await commands.getNotifications(); - if (res.status === 'error') { - throw new Error(res.error) - } + if (res.status === "error") { + throw new Error(res.error); + } - const data: NostrEvent[] = res.data.map((item) => JSON.parse(item)) - const events = data.map((ev) => new LumeEvent(ev)) + const data: NostrEvent[] = res.data.map((item) => JSON.parse(item)); + const events = data.map((ev) => new LumeEvent(ev)); - return events - }, - select: (events) => { - const zaps = new Map() - const reactions = new Map() + return events; + }, + select: (events) => { + const zaps = new Map(); + const reactions = new Map(); + const hex = nip19.decode(id).data; - const texts = events.filter((ev) => ev.kind === Kind.Text) - const zapEvents = events.filter((ev) => ev.kind === Kind.ZapReceipt) - const reactEvents = events.filter( - (ev) => ev.kind === Kind.Repost || ev.kind === Kind.Reaction, - ) + const texts = events.filter( + (ev) => ev.kind === Kind.Text && ev.pubkey !== hex, + ); + const zapEvents = events.filter((ev) => ev.kind === Kind.ZapReceipt); + const reactEvents = events.filter( + (ev) => ev.kind === Kind.Repost || ev.kind === Kind.Reaction, + ); - for (const event of reactEvents) { - const rootId = event.tags.filter((tag) => tag[0] === 'e')[0]?.[1] + for (const event of reactEvents) { + const rootId = event.tags.filter((tag) => tag[0] === "e")[0]?.[1]; - if (rootId) { - if (reactions.has(rootId)) { - reactions.get(rootId).push(event) - } else { - reactions.set(rootId, [event]) - } - } - } + if (rootId) { + if (reactions.has(rootId)) { + reactions.get(rootId).push(event); + } else { + reactions.set(rootId, [event]); + } + } + } - for (const event of zapEvents) { - const rootId = event.tags.filter((tag) => tag[0] === 'e')[0]?.[1] + for (const event of zapEvents) { + const rootId = event.tags.filter((tag) => tag[0] === "e")[0]?.[1]; - if (rootId) { - if (zaps.has(rootId)) { - zaps.get(rootId).push(event) - } else { - zaps.set(rootId, [event]) - } - } - } + if (rootId) { + if (zaps.has(rootId)) { + zaps.get(rootId).push(event); + } else { + zaps.set(rootId, [event]); + } + } + } - return { texts, zaps, reactions } - }, - refetchOnWindowFocus: false, - }) + return { texts, zaps, reactions }; + }, + refetchOnWindowFocus: false, + }); - useEffect(() => { - const unlisten = getCurrentWindow().listen('event', async (data) => { - const event: LumeEvent = JSON.parse(data.payload as string) - await queryClient.setQueryData(['notification'], (data: LumeEvent[]) => [ - event, - ...data, - ]) - }) + useEffect(() => { + const unlisten = getCurrentWindow().listen("event", async (data) => { + const event: LumeEvent = JSON.parse(data.payload as string); + await queryClient.setQueryData( + ["notification", id], + (data: LumeEvent[]) => [event, ...data], + ); + }); - return () => { - unlisten.then((f) => f()) - } - }, []) + return () => { + unlisten.then((f) => f()); + }; + }, [id]); - if (isLoading) { - return ( -
- -
- ) - } + if (isLoading) { + return ( +
+ +
+ ); + } - return ( -
- - - - Replies - - - Reactions - - - Zaps - - - - - {data.texts.map((event) => ( - - ))} - - - {[...data.reactions.entries()].map(([root, events]) => ( -
-
-
- -
-
- {events.map((event) => ( - - - -
- {event.kind === Kind.Reaction ? ( - event.content === '+' ? ( - '👍' - ) : ( - event.content - ) - ) : ( - - )} -
-
-
- ))} -
-
-
- ))} -
- - {[...data.zaps.entries()].map(([root, events]) => ( -
-
-
- -
-
- {events.map((event) => ( - - ))} -
-
-
- ))} -
- - - - -
-
-
- ) + return ( +
+ + + + Replies + + + Reactions + + + Zaps + + + + + {data.texts.map((event) => ( + + ))} + + + {[...data.reactions.entries()].map(([root, events]) => ( +
+
+
+ +
+
+ {events.map((event) => ( + + + +
+ {event.kind === Kind.Reaction ? ( + event.content === "+" ? ( + "👍" + ) : ( + event.content + ) + ) : ( + + )} +
+
+
+ ))} +
+
+
+ ))} +
+ + {[...data.zaps.entries()].map(([root, events]) => ( +
+
+
+ +
+
+ {events.map((event) => ( + + ))} +
+
+
+ ))} +
+ + + + +
+
+
+ ); } function Tab({ value, children }: { value: string; children: ReactNode[] }) { - const ref = useRef(null) + const ref = useRef(null); - return ( - - - {children} - - - ) + return ( + + + {children} + + + ); } function RootNote({ id }: { id: string }) { - const { isLoading, isError, data } = useEvent(id) + const { isLoading, isError, data } = useEvent(id); - if (isLoading) { - return ( -
-
-
-
- ) - } + if (isLoading) { + return ( +
+
+
+
+ ); + } - if (isError || !data) { - return ( -
-
- -
-

- Event not found with your current relay set -

-
- ) - } + if (isError || !data) { + return ( +
+
+ +
+

+ Event not found with your current relay set +

+
+ ); + } - return ( - - - - - - - -
{data.content}
-
-
- ) + return ( + + + + + + + +
{data.content}
+
+
+ ); } function TextNote({ event }: { event: LumeEvent }) { - const pTags = event.tags - .filter((tag) => tag[0] === 'p') - .map((tag) => tag[1]) - .slice(0, 3) + const pTags = event.tags + .filter((tag) => tag[0] === "p") + .map((tag) => tag[1]) + .slice(0, 3); - return ( - - ) + return ( + + ); } function ZapReceipt({ event }: { event: LumeEvent }) { - const amount = useMemo( - () => decodeZapInvoice(event.tags).bitcoinFormatted ?? '0', - [event.id], - ) - const sender = useMemo( - () => event.tags.find((tag) => tag[0] === 'P')?.[1], - [event.id], - ) + const amount = useMemo( + () => decodeZapInvoice(event.tags).bitcoinFormatted ?? "0", + [event.id], + ); + const sender = useMemo( + () => event.tags.find((tag) => tag[0] === "P")?.[1], + [event.id], + ); - if (!sender) { - return ( -
-
-
- ₿ {amount} -
-
- ) - } + if (!sender) { + return ( +
+
+
+ ₿ {amount} +
+
+ ); + } - return ( - - - -
- ₿ {amount} -
-
-
- ) + return ( + + + +
+ ₿ {amount} +
+
+
+ ); } diff --git a/src/system/window.ts b/src/system/window.ts index a1406dfc..87867448 100644 --- a/src/system/window.ts +++ b/src/system/window.ts @@ -28,6 +28,7 @@ export const LumeWindow = { label: "newsfeed", name: "Newsfeed", url: `/columns/newsfeed/${account}`, + account, }, }); }, @@ -35,9 +36,10 @@ export const LumeWindow = { await getCurrentWindow().emit("columns", { type: "add", column: { - label: "newsfeed", - name: "Newsfeed", + label: "stories", + name: "Stories", url: `/columns/stories/${account}`, + account, }, }); }, @@ -48,6 +50,7 @@ export const LumeWindow = { label: "notification", name: "Notification", url: `/columns/notification/${account}`, + account, }, }); }, diff --git a/src/types.ts b/src/types.ts index 78917765..0cbc22f5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -53,11 +53,10 @@ export interface Metadata { export interface LumeColumn { label: string; name: string; - description?: string; url: string; - picture?: string; + description?: string; default?: boolean; - official?: boolean; + account?: string; } export interface ColumnEvent {