mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-29 16:30:55 +00:00
wip: basic support multi windows
This commit is contained in:
parent
a0d9a729dd
commit
296b11b7b8
@ -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>
|
||||
)}
|
||||
|
11
apps/desktop2/src/routes/events/$eventId.lazy.tsx
Normal file
11
apps/desktop2/src/routes/events/$eventId.lazy.tsx
Normal 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>;
|
||||
}
|
11
apps/desktop2/src/routes/users/$pubkey.lazy.tsx
Normal file
11
apps/desktop2/src/routes/users/$pubkey.lazy.tsx
Normal 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>;
|
||||
}
|
@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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" />
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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}`}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
)}
|
||||
/>
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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/*"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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"]}}
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user