wip: basic support multi windows

This commit is contained in:
reya 2024-02-16 09:58:07 +07:00
parent a0d9a729dd
commit 296b11b7b8
17 changed files with 214 additions and 118 deletions

View File

@ -1,7 +1,7 @@
import { useArk } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } from "@lume/icons";
import { Event, Kind } from "@lume/types";
import { EmptyFeed, TextNote } from "@lume/ui";
import { EmptyFeed, RepostNote, TextNote } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { useInfiniteQuery } from "@tanstack/react-query";
import { createLazyFileRoute } from "@tanstack/react-router";
@ -18,13 +18,13 @@ function Home() {
queryKey: ["timeline"],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_text_events(FETCH_LIMIT, pageParam);
const events = await ark.get_text_events(FETCH_LIMIT, pageParam, true);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at;
return lastEvent.created_at - 1;
},
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
@ -32,8 +32,8 @@ function Home() {
const renderItem = (event: Event) => {
switch (event.kind) {
case Kind.Text:
return <TextNote key={event.id} event={event} />;
case Kind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
}
@ -59,7 +59,7 @@ function Home() {
</a>
</div>
) : (
<Virtualizer overscan={2}>
<Virtualizer overscan={3}>
{data.map((item) => renderItem(item))}
</Virtualizer>
)}

View File

@ -0,0 +1,11 @@
import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/events/$eventId")({
component: Event,
});
function Event() {
const { eventId } = Route.useParams();
return <div>{eventId}</div>;
}

View File

@ -0,0 +1,11 @@
import { createLazyFileRoute } from "@tanstack/react-router";
export const Route = createLazyFileRoute("/users/$pubkey")({
component: User,
});
function User() {
const { pubkey } = Route.useParams();
return <div>{pubkey}</div>;
}

View File

@ -1,5 +1,6 @@
import type { CurrentAccount, Event, Keys, Metadata } from "@lume/types";
import { invoke } from "@tauri-apps/api/core";
import { WebviewWindow } from "@tauri-apps/api/webview";
export class Ark {
public account: CurrentAccount;
@ -59,21 +60,56 @@ export class Ark {
public async get_event(id: string) {
try {
const cmd: string = await invoke("get_event", { id });
const eventId: string = id
.replace("nostr:", "")
.split("'")[0]
.split(".")[0];
const cmd: string = await invoke("get_event", { id: eventId });
const event = JSON.parse(cmd) as Event;
return event;
} catch (e) {
return null;
}
}
public async get_text_events(limit: number, asOf?: number) {
public async get_text_events(limit: number, asOf?: number, dedup?: boolean) {
try {
let until: string = undefined;
if (asOf && asOf > 0) until = asOf.toString();
const cmd: Event[] = await invoke("get_text_events", { limit, until });
return cmd;
const seenIds = new Set<string>();
const dedupQueue = new Set<string>();
const nostrEvents: Event[] = await invoke("get_text_events", {
limit,
until,
});
if (dedup) {
for (const event of nostrEvents) {
const tags = event.tags
.filter((el) => el[0] === "e")
?.map((item) => item[1]);
if (tags.length) {
for (const tag of tags) {
if (seenIds.has(tag)) {
dedupQueue.add(event.id);
break;
}
seenIds.add(tag);
}
}
}
return nostrEvents
.filter((event) => !dedupQueue.has(event.id))
.sort((a, b) => b.created_at - a.created_at);
}
return nostrEvents.sort((a, b) => b.created_at - a.created_at);
} catch (e) {
return [];
}
@ -170,9 +206,16 @@ export class Ark {
};
}
public async get_profile(id: string) {
public async get_profile(pubkey: string) {
try {
const id = pubkey
.replace("nostr:", "")
.split("'")[0]
.split(".")[0]
.split(",")[0]
.split("?")[0];
const cmd: Metadata = await invoke("get_profile", { id });
return cmd;
} catch {
return null;
@ -202,4 +245,26 @@ export class Ark {
return false;
}
}
public open_thread(id: string) {
return new WebviewWindow(`event-${id}`, {
title: "Thread",
url: `/events/${id}`,
width: 600,
height: 800,
hiddenTitle: true,
titleBarStyle: "overlay",
});
}
public open_profile(pubkey: string) {
return new WebviewWindow(`user-${pubkey}`, {
title: "Profile",
url: `/users/${pubkey}`,
width: 600,
height: 800,
hiddenTitle: true,
titleBarStyle: "overlay",
});
}
}

View File

@ -2,9 +2,12 @@ import { ReplyIcon } from "@lume/icons";
import * as Tooltip from "@radix-ui/react-tooltip";
import { useTranslation } from "react-i18next";
import { useNoteContext } from "../provider";
import { useArk } from "@lume/ark";
export function NoteReply() {
const ark = useArk();
const event = useNoteContext();
const { t } = useTranslation();
return (
@ -13,6 +16,7 @@ export function NoteReply() {
<Tooltip.Trigger asChild>
<button
type="button"
onClick={() => ark.open_thread(event.id)}
className="group inline-flex h-7 w-7 items-center justify-center text-neutral-800 dark:text-neutral-200"
>
<ReplyIcon className="size-5 group-hover:text-blue-500" />

View File

@ -110,7 +110,7 @@ export function NoteChild({
</div>
<User.Provider pubkey={data.pubkey}>
<User.Root>
<User.Avatar className="size-10 shrink-0 rounded-lg object-cover" />
<User.Avatar className="size-10 shrink-0 rounded-full object-cover" />
<div className="absolute left-2 top-2 inline-flex items-center gap-1.5 font-semibold leading-tight">
<User.Name className="max-w-[10rem] truncate" />
<div className="font-normal text-neutral-700 dark:text-neutral-300">

View File

@ -32,10 +32,7 @@ export function NoteMenu() {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger asChild>
<button
type="button"
className="inline-flex size-6 items-center justify-center"
>
<button type="button">
<HorizontalDotsIcon className="size-4 hover:text-blue-500 dark:text-neutral-200" />
</button>
</DropdownMenu.Trigger>

View File

@ -73,17 +73,17 @@ export function RepostNote({
return (
<Note.Root
className={cn(
"flex flex-col rounded-xl bg-neutral-50 dark:bg-neutral-950",
"mb-3 flex flex-col gap-2 border-b border-neutral-100 pb-3 dark:border-neutral-900",
className,
)}
>
<User.Provider pubkey={event.pubkey}>
<User.Root className="flex h-14 gap-2 px-3">
<User.Root className="flex gap-3">
<div className="inline-flex w-10 shrink-0 items-center justify-center">
<RepostIcon className="h-5 w-5 text-blue-500" />
</div>
<div className="inline-flex items-center gap-2">
<User.Avatar className="size-6 shrink-0 rounded object-cover" />
<User.Avatar className="size-6 shrink-0 rounded-full object-cover" />
<div className="inline-flex items-baseline gap-1">
<User.Name className="font-medium text-neutral-900 dark:text-neutral-100" />
<span className="text-blue-500">{t("note.reposted")}</span>
@ -92,18 +92,23 @@ export function RepostNote({
</User.Root>
</User.Provider>
<Note.Provider event={repostEvent}>
<div className="relative flex flex-col gap-2 px-3">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-2">
<div className="flex items-start justify-between">
<Note.User className="flex-1 pr-2" />
<Note.Menu />
</div>
<Note.Content />
<div className="flex h-14 items-center justify-between">
<Note.Pin />
<div className="inline-flex items-center gap-4">
<Note.Reply />
<Note.Repost />
<Note.Zap />
<div className="flex gap-3">
<div className="size-10 shrink-0" />
<div className="min-w-0 flex-1">
<Note.Content />
<div className="mt-5 flex items-center justify-between">
<Note.Reaction />
<div className="inline-flex items-center gap-4">
<Note.Reply />
<Note.Repost />
<Note.Zap />
</div>
</div>
</div>
</div>
</div>

View File

@ -24,8 +24,8 @@ export function TextNote({
<div className="flex gap-3">
<div className="size-10 shrink-0" />
<div className="min-w-0 flex-1">
<Note.Content className="mb-2" />
<Note.Thread className="mb-2" />
<Note.Content />
<div className="mt-5 flex items-center justify-between">
<Note.Reaction />
<div className="inline-flex items-center gap-4">

View File

@ -20,12 +20,12 @@ export function NoteThread({ className }: { className?: string }) {
return (
<div className={cn("w-full", className)}>
<div className="flex h-min w-full flex-col gap-3 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
{thread.replyEventId ? (
<Note.Child eventId={thread.replyEventId} />
) : null}
{thread.rootEventId ? (
<Note.Child eventId={thread.rootEventId} isRoot />
) : null}
{thread.replyEventId ? (
<Note.Child eventId={thread.replyEventId} />
) : null}
<div className="inline-flex items-center justify-between">
<a
href={`/events/${thread?.rootEventId || thread?.replyEventId}`}

View File

@ -2,22 +2,22 @@ import { cn } from "@lume/utils";
import { useUserContext } from "./provider";
export function UserName({ className }: { className?: string }) {
const user = useUserContext();
const user = useUserContext();
if (!user.profile) {
return (
<div
className={cn(
"h-4 w-20 self-center bg-black/20 dark:bg-white/20 rounded animate-pulse",
className,
)}
/>
);
}
if (!user.profile) {
return (
<div
className={cn(
"mb-1 h-3 w-20 animate-pulse self-center rounded bg-black/20 dark:bg-white/20",
className,
)}
/>
);
}
return (
<div className={cn("max-w-[12rem] truncate", className)}>
{user.profile.display_name || user.profile.name || "Anon"}
</div>
);
return (
<div className={cn("max-w-[12rem] truncate", className)}>
{user.profile.display_name || user.profile.name || "Anon"}
</div>
);
}

View File

@ -13,7 +13,6 @@ export function UserNip05({ className }: { className?: string }) {
queryFn: async () => {
if (!user.profile?.nip05) return false;
const verify = await ark.verify_nip05(user.pubkey, user.profile?.nip05);
console.log(verify);
return verify;
},
enabled: !!user.profile,
@ -23,7 +22,7 @@ export function UserNip05({ className }: { className?: string }) {
return (
<div
className={cn(
"h-4 w-20 animate-pulse rounded bg-black/20 dark:bg-white/20",
"h-3 w-20 animate-pulse rounded bg-black/20 dark:bg-white/20",
className,
)}
/>

View File

@ -2,10 +2,13 @@ import { cn, formatCreatedAt } from "@lume/utils";
import { useMemo } from "react";
export function UserTime({
time,
className,
}: { time: number; className?: string }) {
const createdAt = useMemo(() => formatCreatedAt(time), [time]);
time,
className,
}: {
time: number;
className?: string;
}) {
const createdAt = useMemo(() => formatCreatedAt(time), [time]);
return <div className={cn("", className)}>{createdAt}</div>;
return <div className={cn("leading-tight", className)}>{createdAt}</div>;
}

View File

@ -1,46 +1,49 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"description": "Capability for the desktop",
"platforms": ["linux", "macOS", "windows"],
"windows": ["main", "settings", "event-*", "user-*", "column-*"],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"theme:allow-set-theme",
"theme:allow-get-theme",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:default",
"os:allow-locale",
"os:allow-platform",
"updater:allow-check",
"updater:default",
"window:allow-start-dragging",
"store:allow-get",
{
"identifier": "http:default",
"allow": [
{
"url": "http://**/"
},
{
"url": "https://**/"
}
]
},
{
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "$RESOURCE/locales/*"
}
]
}
]
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"description": "Capability for the desktop",
"platforms": ["linux", "macOS", "windows"],
"windows": ["main", "settings", "event-*", "user-*", "column-*"],
"permissions": [
"path:default",
"event:default",
"window:default",
"app:default",
"resources:default",
"menu:default",
"tray:default",
"theme:allow-set-theme",
"theme:allow-get-theme",
"notification:allow-is-permission-granted",
"notification:allow-request-permission",
"notification:default",
"os:allow-locale",
"os:allow-platform",
"updater:allow-check",
"updater:default",
"window:allow-start-dragging",
"store:allow-get",
"clipboard-manager:allow-write",
"clipboard-manager:allow-read",
"webview:allow-create-webview-window",
{
"identifier": "http:default",
"allow": [
{
"url": "http://**/"
},
{
"url": "https://**/"
}
]
},
{
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "$RESOURCE/locales/*"
}
]
}
]
}

View File

@ -1 +1 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","context":"local","windows":["main","settings","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","theme:allow-set-theme","theme:allow-get-theme","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","store:allow-get",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}}
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","context":"local","windows":["main","settings","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","theme:allow-set-theme","theme:allow-get-theme","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}}

View File

@ -6,15 +6,14 @@ use tauri::State;
#[tauri::command(async)]
pub async fn get_event(id: &str, nostr: State<'_, Nostr>) -> Result<String, String> {
let client = &nostr.client;
let event_id;
if id.starts_with("note") {
event_id = EventId::from_bech32(id).unwrap();
} else if id.starts_with("nevent") {
event_id = Nip19Event::from_bech32(id).unwrap().event_id;
} else {
event_id = EventId::from_hex(id).unwrap();
}
let event_id: EventId = match Nip19::from_bech32(id) {
Ok(val) => match val {
Nip19::EventId(id) => id,
Nip19::Event(event) => event.event_id,
_ => panic!("not nip19"),
},
Err(_) => EventId::from_hex(id).unwrap(),
};
let filter = Filter::new().id(event_id);
let events = client

View File

@ -6,15 +6,14 @@ use tauri::State;
#[tauri::command(async)]
pub async fn get_profile(id: &str, nostr: State<'_, Nostr>) -> Result<Metadata, ()> {
let client = &nostr.client;
let public_key;
if id.starts_with("nprofile1") {
public_key = XOnlyPublicKey::from_bech32(id).unwrap();
} else if id.starts_with("npub1") {
public_key = XOnlyPublicKey::from_bech32(id).unwrap();
} else {
public_key = XOnlyPublicKey::from_str(id).unwrap();
}
let public_key: XOnlyPublicKey = match Nip19::from_bech32(id) {
Ok(val) => match val {
Nip19::Pubkey(pubkey) => pubkey,
Nip19::Profile(profile) => profile.public_key,
_ => panic!("not nip19"),
},
Err(_) => XOnlyPublicKey::from_str(id).unwrap(),
};
let filter = Filter::new()
.author(public_key)