Compare commits

..
4 Commits
Author SHA1 Message Date
reya e4a317f038 chore: bump version 2024-06-20 13:23:05 +07:00
reya 9779d020c7 feat: improve list virtualization 2024-06-20 13:22:28 +07:00
雨宮蓮andGitHub f8280ec8ee fix: get replies function (#213) 2024-06-19 21:02:33 +07:00
XIAO YUandGitHub 6c26f8967b chore: Refactor code for better performance and reliability (#212) 2024-06-19 14:36:59 +07:00
24 changed files with 684 additions and 399 deletions
+1
View File
@@ -20,6 +20,7 @@
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-scroll-area": "^1.1.0",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7",
-12
View File
@@ -2,18 +2,6 @@
@tailwind utilities; @tailwind utilities;
@tailwind components; @tailwind components;
*::-webkit-scrollbar {
@apply w-[5px];
}
*::-webkit-scrollbar-track {
@apply bg-transparent;
}
*::-webkit-scrollbar-thumb {
@apply rounded bg-black dark:bg-white;
}
@layer utilities { @layer utilities {
.content-break { .content-break {
word-break: break-word; word-break: break-word;
@@ -96,7 +96,9 @@ export function NoteContent({
<div <div
className={cn( className={cn(
"select-text text-pretty content-break overflow-hidden", "select-text text-pretty content-break overflow-hidden",
event.content.length > 620 ? "max-h-[250px] gradient-mask-b-0" : "", event.meta?.content.length > 400
? "max-h-[250px] gradient-mask-b-0"
: "",
className, className,
)} )}
> >
@@ -42,8 +42,8 @@ export function Images({ urls }: { urls: string[] }) {
return ( return (
<Carousel <Carousel
items={imageUrls} items={imageUrls}
renderItem={({ item, isSnapPoint }) => ( renderItem={({ item, index, isSnapPoint }) => (
<CarouselItem key={item} isSnapPoint={isSnapPoint}> <CarouselItem key={item + index} isSnapPoint={isSnapPoint}>
<img <img
src={item} src={item}
alt={item} alt={item}
+55 -42
View File
@@ -1,69 +1,82 @@
import { Note } from "@/components/note"; import { Note } from "@/components/note";
import { type LumeEvent, NostrQuery, useEvent } from "@lume/system"; import { type LumeEvent, NostrQuery } from "@lume/system";
import { Box, Container, Spinner } from "@lume/ui"; import { Box, Container, Spinner } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useState } from "react";
import { WindowVirtualizer } from "virtua"; import { WindowVirtualizer } from "virtua";
import { ReplyList } from "./-components/replyList"; import { Reply } from "./-components/reply";
export const Route = createFileRoute("/events/$eventId")({ export const Route = createFileRoute("/events/$eventId")({
beforeLoad: async () => { beforeLoad: async () => {
const settings = await NostrQuery.getUserSettings(); const settings = await NostrQuery.getUserSettings();
return { settings }; return { settings };
}, },
loader: async ({ params }) => {
const event = await NostrQuery.getEvent(params.eventId);
return event;
},
component: Screen, component: Screen,
}); });
function Screen() { function Screen() {
const { eventId } = Route.useParams(); const event = Route.useLoaderData();
const { isLoading, isError, data } = useEvent(eventId);
if (isLoading) { const [reload, setReload] = useState(false);
return ( const [replies, setReplies] = useState<LumeEvent[]>(null);
<div className="flex items-center justify-center w-full h-full">
<Spinner className="size-5" />
</div>
);
}
if (isError) { useEffect(() => {
<div className="flex items-center justify-center w-full h-full"> let mounted = true;
<p>Not found.</p>
</div>; if (event) {
} event.getAllReplies().then((data) => {
if (mounted) setReplies(data);
});
}
return () => {
mounted = false;
};
}, [event]);
return ( return (
<Container withDrag> <Container withDrag>
<Box className="scrollbar-none"> <Box className="scrollbar-none">
<WindowVirtualizer> <WindowVirtualizer>
<MainNote data={data} /> <Note.Provider event={event}>
{data ? ( <Note.Root>
<ReplyList eventId={eventId} /> <div className="flex items-center justify-between px-3 h-14">
) : ( <Note.User />
<div className="flex items-center justify-center w-full h-full"> <Note.Menu />
<Spinner className="size-5" /> </div>
<Note.ContentLarge className="px-3" />
<div className="flex items-center justify-end gap-2 px-3 mt-4 h-11">
<Note.Reply large />
<Note.Repost large />
<Note.Zap large />
</div>
</Note.Root>
</Note.Provider>
<div className="flex flex-col">
<div className="flex items-center px-3 text-sm font-semibold border-t h-11 text-neutral-700 dark:text-neutral-300 border-neutral-100 dark:border-neutral-900">
Replies ({replies?.length ?? 0})
</div> </div>
)} {!replies ? (
<Spinner />
) : !replies.length ? (
<div className="flex items-center justify-center w-full">
<div className="flex flex-col items-center justify-center gap-2 py-6">
<h3 className="text-3xl">👋</h3>
<p className="leading-none text-neutral-600 dark:text-neutral-400">
Be the first to Reply!
</p>
</div>
</div>
) : (
replies.map((event) => <Reply key={event.id} event={event} />)
)}
</div>
</WindowVirtualizer> </WindowVirtualizer>
</Box> </Box>
</Container> </Container>
); );
} }
function MainNote({ data }: { data: LumeEvent }) {
return (
<Note.Provider event={data}>
<Note.Root>
<div className="flex items-center justify-between px-3 h-14">
<Note.User />
<Note.Menu />
</div>
<Note.ContentLarge className="px-3" />
<div className="flex items-center justify-end gap-2 px-3 mt-4 h-11">
<Note.Reply large />
<Note.Repost large />
<Note.Zap large />
</div>
</Note.Root>
</Note.Provider>
);
}
@@ -1,18 +1,18 @@
import type { EventWithReplies } from "@lume/types"; import { Note } from "@/components/note";
import type { LumeEvent } from "@lume/system";
import { cn } from "@lume/utils"; import { cn } from "@lume/utils";
import { SubReply } from "./subReply"; import { SubReply } from "./subReply";
import { Note } from "@/components/note";
export function Reply({ event }: { event: EventWithReplies }) { export function Reply({ event }: { event: LumeEvent }) {
return ( return (
<Note.Provider event={event}> <Note.Provider event={event}>
<Note.Root className="border-t border-neutral-100 dark:border-neutral-900"> <Note.Root className="border-t border-neutral-100 dark:border-neutral-900">
<div className="px-3 h-14 flex items-center justify-between"> <div className="flex items-center justify-between px-3 h-14">
<Note.User /> <Note.User />
<Note.Menu /> <Note.Menu />
</div> </div>
<Note.ContentLarge className="px-3" /> <Note.ContentLarge className="px-3" />
<div className="mt-3 flex items-center gap-4 px-3 h-14"> <div className="flex items-center gap-4 px-3 mt-3 h-14">
<Note.Reply /> <Note.Reply />
<Note.Repost /> <Note.Repost />
<Note.Zap /> <Note.Zap />
+61 -43
View File
@@ -6,9 +6,10 @@ import { ArrowRightCircleIcon } from "@lume/icons";
import { type LumeEvent, NostrQuery } from "@lume/system"; import { type LumeEvent, NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, Kind } from "@lume/types"; import { type ColumnRouteSearch, Kind } from "@lume/types";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { useCallback } from "react"; import { useCallback, useRef } from "react";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
export const Route = createFileRoute("/global")({ export const Route = createFileRoute("/global")({
@@ -47,6 +48,8 @@ export function Screen() {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const ref = useRef<HTMLDivElement>(null);
const renderItem = useCallback( const renderItem = useCallback(
(event: LumeEvent) => { (event: LumeEvent) => {
if (!event) return; if (!event) return;
@@ -70,48 +73,63 @@ export function Screen() {
); );
return ( return (
<div className="w-full h-full p-3 overflow-y-auto scrollbar-none"> <ScrollArea.Root
{isFetching && !isLoading && !isFetchingNextPage ? ( type={"scroll"}
<div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"> scrollHideDelay={300}
<div className="flex items-center justify-center gap-2"> className="overflow-hidden size-full"
<Spinner className="size-5" /> >
<span className="text-sm font-medium">Fetching new notes...</span> <ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
</div> <Virtualizer scrollRef={ref}>
</div> {isFetching && !isLoading && !isFetchingNextPage ? (
) : null} <div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
{isLoading ? ( <div className="flex items-center justify-center gap-2">
<div className="flex items-center justify-center w-full h-16 gap-2"> <Spinner className="size-5" />
<Spinner className="size-5" /> <span className="text-sm font-medium">
<span className="text-sm font-medium">Loading...</span> Fetching new notes...
</div> </span>
) : !data.length ? ( </div>
<div className="flex items-center justify-center"> </div>
Yo. You're catching up on all the things happening around you. ) : null}
</div> {isLoading ? (
) : ( <div className="flex items-center justify-center w-full h-16 gap-2">
<Virtualizer overscan={3}>
{data.map((item) => renderItem(item))}
</Virtualizer>
)}
{data?.length && hasNextPage ? (
<div>
<button
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
>
{isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-5" />
) : ( <span className="text-sm font-medium">Loading...</span>
<> </div>
<ArrowRightCircleIcon className="size-5" /> ) : !data.length ? (
Load more <div className="flex items-center justify-center">
</> Yo. You're catching up on all the things happening around you.
)} </div>
</button> ) : (
</div> data.map((item) => renderItem(item))
) : null} )}
</div> {data?.length && hasNextPage ? (
<div>
<button
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
>
{isFetchingNextPage ? (
<Spinner className="size-5" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
</div>
) : null}
</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
); );
} }
+61 -43
View File
@@ -6,9 +6,10 @@ import { ArrowRightCircleIcon } from "@lume/icons";
import { type LumeEvent, NostrQuery } from "@lume/system"; import { type LumeEvent, NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, Kind } from "@lume/types"; import { type ColumnRouteSearch, Kind } from "@lume/types";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
import { useCallback } from "react"; import { useCallback, useRef } from "react";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
export const Route = createFileRoute("/group")({ export const Route = createFileRoute("/group")({
@@ -61,6 +62,8 @@ export function Screen() {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const ref = useRef<HTMLDivElement>(null);
const renderItem = useCallback( const renderItem = useCallback(
(event: LumeEvent) => { (event: LumeEvent) => {
if (!event) return; if (!event) return;
@@ -84,48 +87,63 @@ export function Screen() {
); );
return ( return (
<div className="w-full h-full p-3 overflow-y-auto scrollbar-none"> <ScrollArea.Root
{isFetching && !isLoading && !isFetchingNextPage ? ( type={"scroll"}
<div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"> scrollHideDelay={300}
<div className="flex items-center justify-center gap-2"> className="overflow-hidden size-full"
<Spinner className="size-5" /> >
<span className="text-sm font-medium">Fetching new notes...</span> <ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
</div> <Virtualizer scrollRef={ref}>
</div> {isFetching && !isLoading && !isFetchingNextPage ? (
) : null} <div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
{isLoading ? ( <div className="flex items-center justify-center gap-2">
<div className="flex items-center justify-center w-full h-16 gap-2"> <Spinner className="size-5" />
<Spinner className="size-5" /> <span className="text-sm font-medium">
<span className="text-sm font-medium">Loading...</span> Fetching new notes...
</div> </span>
) : !data.length ? ( </div>
<div className="flex items-center justify-center"> </div>
Yo. You're catching up on all the things happening around you. ) : null}
</div> {isLoading ? (
) : ( <div className="flex items-center justify-center w-full h-16 gap-2">
<Virtualizer overscan={3}>
{data.map((item) => renderItem(item))}
</Virtualizer>
)}
{data?.length && hasNextPage ? (
<div>
<button
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
>
{isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-5" />
) : ( <span className="text-sm font-medium">Loading...</span>
<> </div>
<ArrowRightCircleIcon className="size-5" /> ) : !data.length ? (
Load more <div className="flex items-center justify-center">
</> Yo. You're catching up on all the things happening around you.
)} </div>
</button> ) : (
</div> data.map((item) => renderItem(item))
) : null} )}
</div> {data?.length && hasNextPage ? (
<div>
<button
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
>
{isFetchingNextPage ? (
<Spinner className="size-5" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
</div>
) : null}
</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
); );
} }
+61 -43
View File
@@ -6,9 +6,10 @@ import { ArrowRightCircleIcon } from "@lume/icons";
import { type LumeEvent, NostrAccount, NostrQuery } from "@lume/system"; import { type LumeEvent, NostrAccount, NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, Kind } from "@lume/types"; import { type ColumnRouteSearch, Kind } from "@lume/types";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
import { useCallback } from "react"; import { useCallback, useRef } from "react";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
export const Route = createFileRoute("/newsfeed")({ export const Route = createFileRoute("/newsfeed")({
@@ -59,6 +60,8 @@ export function Screen() {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const ref = useRef<HTMLDivElement>(null);
const renderItem = useCallback( const renderItem = useCallback(
(event: LumeEvent) => { (event: LumeEvent) => {
if (!event) return; if (!event) return;
@@ -82,48 +85,63 @@ export function Screen() {
); );
return ( return (
<div className="w-full h-full p-3 overflow-y-auto scrollbar-none"> <ScrollArea.Root
{isFetching && !isLoading && !isFetchingNextPage ? ( type={"scroll"}
<div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"> scrollHideDelay={300}
<div className="flex items-center justify-center gap-2"> className="overflow-hidden size-full"
<Spinner className="size-5" /> >
<span className="text-sm font-medium">Fetching new notes...</span> <ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
</div> <Virtualizer scrollRef={ref}>
</div> {isFetching && !isLoading && !isFetchingNextPage ? (
) : null} <div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
{isLoading ? ( <div className="flex items-center justify-center gap-2">
<div className="flex items-center justify-center w-full h-16 gap-2"> <Spinner className="size-5" />
<Spinner className="size-5" /> <span className="text-sm font-medium">
<span className="text-sm font-medium">Loading...</span> Fetching new notes...
</div> </span>
) : !data.length ? ( </div>
<div className="flex items-center justify-center"> </div>
Yo. You're catching up on all the things happening around you. ) : null}
</div> {isLoading ? (
) : ( <div className="flex items-center justify-center w-full h-16 gap-2">
<Virtualizer overscan={3}>
{data.map((item) => renderItem(item))}
</Virtualizer>
)}
{data?.length && hasNextPage ? (
<div>
<button
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
>
{isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-5" />
) : ( <span className="text-sm font-medium">Loading...</span>
<> </div>
<ArrowRightCircleIcon className="size-5" /> ) : !data.length ? (
Load more <div className="flex items-center justify-center">
</> Yo. You're catching up on all the things happening around you.
)} </div>
</button> ) : (
</div> data.map((item) => renderItem(item))
) : null} )}
</div> {data?.length && hasNextPage ? (
<div>
<button
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
>
{isFetchingNextPage ? (
<Spinner className="size-5" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
</div>
) : null}
</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
); );
} }
+84 -55
View File
@@ -13,13 +13,22 @@ import {
decodeZapInvoice, decodeZapInvoice,
formatCreatedAt, formatCreatedAt,
} from "@lume/utils"; } from "@lume/utils";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import * as Tabs from "@radix-ui/react-tabs"; import * as Tabs from "@radix-ui/react-tabs";
import { createFileRoute } from "@tanstack/react-router"; import { createFileRoute } from "@tanstack/react-router";
import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu"; import { Menu, MenuItem, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { getCurrent } from "@tauri-apps/api/window"; import { getCurrent } from "@tauri-apps/api/window";
import { exit } from "@tauri-apps/plugin-process"; import { exit } from "@tauri-apps/plugin-process";
import { open } from "@tauri-apps/plugin-shell"; import { open } from "@tauri-apps/plugin-shell";
import { useCallback, useEffect, useMemo, useState } from "react"; import {
type ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Virtualizer } from "virtua";
interface EmitAccount { interface EmitAccount {
account: string; account: string;
@@ -161,7 +170,7 @@ function Screen() {
return ( return (
<div className="flex flex-col w-full h-full"> <div className="flex flex-col w-full h-full">
<div className="flex items-center justify-between px-4 border-b h-11 shrink-0 border-black/5"> <div className="flex items-center justify-between px-4 border-b h-11 shrink-0 border-black/5 dark:border-white/5">
<div> <div>
<h1 className="text-sm font-semibold">Notifications</h1> <h1 className="text-sm font-semibold">Notifications</h1>
</div> </div>
@@ -193,35 +202,36 @@ function Screen() {
> >
<Tabs.List className="flex items-center"> <Tabs.List className="flex items-center">
<Tabs.Trigger <Tabs.Trigger
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-2 text-sm font-medium border-b border-black/10 data-[state=active]:border-black/30 dark:data-[state=active] data-[state=inactive]:opacity-50" className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-2 text-sm font-medium border-b border-black/10 dark:border-white/10 data-[state=active]:border-black/30 dark:data-[state=active]:border-white/30 data-[state=inactive]:opacity-50"
value="replies" value="replies"
> >
Replies Replies
</Tabs.Trigger> </Tabs.Trigger>
<Tabs.Trigger <Tabs.Trigger
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-2 text-sm font-medium border-b border-black/10 data-[state=active]:border-black/30 dark:data-[state=active] data-[state=inactive]:opacity-50" className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-2 text-sm font-medium border-b border-black/10 dark:border-white/10 data-[state=active]:border-black/30 dark:data-[state=active]:border-white/30 data-[state=inactive]:opacity-50"
value="reactions" value="reactions"
> >
Reactions Reactions
</Tabs.Trigger> </Tabs.Trigger>
<Tabs.Trigger <Tabs.Trigger
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-2 text-sm font-medium border-b border-black/10 data-[state=active]:border-black/30 dark:data-[state=active] data-[state=inactive]:opacity-50" className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-2 text-sm font-medium border-b border-black/10 dark:border-white/10 data-[state=active]:border-black/30 dark:data-[state=active]:border-white/30 data-[state=inactive]:opacity-50"
value="zaps" value="zaps"
> >
Zaps Zaps
</Tabs.Trigger> </Tabs.Trigger>
</Tabs.List> </Tabs.List>
<div className="p-2"> <div className="h-full">
<Tabs.Content value="replies" className="flex flex-col gap-2"> <Tab value="replies">
{texts.map((event) => ( {texts.map((event, index) => (
<TextNote key={event.id} event={event} /> // biome-ignore lint/suspicious/noArrayIndexKey: <explanation>
<TextNote key={event.id + index} event={event} />
))} ))}
</Tabs.Content> </Tab>
<Tabs.Content value="reactions" className="flex flex-col gap-2"> <Tab value="reactions">
{[...reactions.entries()].map(([root, events]) => ( {[...reactions.entries()].map(([root, events]) => (
<div <div
key={root} key={root}
className="flex flex-col gap-1 p-2 rounded-lg shrink-0 backdrop-blur-md bg-black/10 dark:bg-white/10" className="flex flex-col gap-1 p-2 mb-2 rounded-lg shrink-0 backdrop-blur-md bg-black/10 dark:bg-white/10"
> >
<div className="flex flex-col flex-1 min-w-0 gap-2"> <div className="flex flex-col flex-1 min-w-0 gap-2">
<div className="flex items-center gap-2 pb-2 border-b border-black/5 dark:border-white/5"> <div className="flex items-center gap-2 pb-2 border-b border-black/5 dark:border-white/5">
@@ -250,12 +260,12 @@ function Screen() {
</div> </div>
</div> </div>
))} ))}
</Tabs.Content> </Tab>
<Tabs.Content value="zaps" className="flex flex-col gap-2"> <Tab value="zaps">
{[...zaps.entries()].map(([root, events]) => ( {[...zaps.entries()].map(([root, events]) => (
<div <div
key={root} key={root}
className="flex flex-col gap-1 p-2 rounded-lg shrink-0 backdrop-blur-md bg-black/10 dark:bg-white/10" className="flex flex-col gap-1 p-2 mb-2 rounded-lg shrink-0 backdrop-blur-md bg-black/10 dark:bg-white/10"
> >
<div className="flex flex-col flex-1 min-w-0 gap-2"> <div className="flex flex-col flex-1 min-w-0 gap-2">
<div className="flex items-center gap-2 pb-2 border-b border-black/5 dark:border-white/5"> <div className="flex items-center gap-2 pb-2 border-b border-black/5 dark:border-white/5">
@@ -279,13 +289,38 @@ function Screen() {
</div> </div>
</div> </div>
))} ))}
</Tabs.Content> </Tab>
</div> </div>
</Tabs.Root> </Tabs.Root>
</div> </div>
); );
} }
function Tab({ value, children }: { value: string; children: ReactNode[] }) {
const ref = useRef<HTMLDivElement>(null);
return (
<Tabs.Content value={value} className="size-full">
<ScrollArea.Root
type={"scroll"}
scrollHideDelay={300}
className="overflow-hidden size-full"
>
<ScrollArea.Viewport ref={ref} className="h-full px-2 pt-2">
<Virtualizer scrollRef={ref}>{children}</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
</Tabs.Content>
);
}
function RootNote({ id }: { id: string }) { function RootNote({ id }: { id: string }) {
const { isLoading, isError, data } = useEvent(id); const { isLoading, isError, data } = useEvent(id);
@@ -332,46 +367,40 @@ function TextNote({ event }: { event: LumeEvent }) {
.slice(0, 3); .slice(0, 3);
return ( return (
<button <Note.Provider event={event}>
type="button" <Note.Root className="flex flex-col p-2 mb-2 rounded-lg shrink-0 backdrop-blur-md bg-black/10 dark:bg-white/10">
key={event.id} <User.Provider pubkey={event.pubkey}>
onClick={() => LumeWindow.openEvent(event)} <User.Root className="inline-flex items-center gap-2">
> <User.Avatar className="rounded-full size-9 shrink-0" />
<Note.Provider event={event}> <div className="flex flex-col flex-1">
<Note.Root className="flex flex-col p-2 rounded-lg shrink-0 backdrop-blur-md bg-black/10 dark:bg-white/10"> <div className="flex items-baseline justify-between w-full">
<User.Provider pubkey={event.pubkey}> <User.Name className="text-sm font-semibold leading-tight" />
<User.Root className="inline-flex items-center gap-2"> <span className="text-sm leading-tight text-black/50 dark:text-white/50">
<User.Avatar className="rounded-full size-9 shrink-0" /> {formatCreatedAt(event.created_at)}
<div className="flex flex-col flex-1"> </span>
<div className="flex items-baseline justify-between w-full"> </div>
<User.Name className="text-sm font-semibold leading-tight" /> <div className="inline-flex items-baseline gap-1 text-xs">
<span className="text-sm leading-tight text-black/50 dark:text-white/50"> <span className="leading-tight text-black/50 dark:text-white/50">
{formatCreatedAt(event.created_at)} Reply to:
</span> </span>
</div> <div className="inline-flex items-baseline gap-1">
<div className="inline-flex items-baseline gap-1 text-xs"> {pTags.map((replyTo) => (
<span className="leading-tight text-black/50 dark:text-white/50"> <User.Provider key={replyTo} pubkey={replyTo}>
Reply to: <User.Root>
</span> <User.Name className="font-medium leading-tight" />
<div className="inline-flex items-baseline gap-1"> </User.Root>
{pTags.map((replyTo) => ( </User.Provider>
<User.Provider key={replyTo} pubkey={replyTo}> ))}
<User.Root>
<User.Name className="font-medium leading-tight" />
</User.Root>
</User.Provider>
))}
</div>
</div> </div>
</div> </div>
</User.Root> </div>
</User.Provider> </User.Root>
<div className="flex gap-2"> </User.Provider>
<div className="w-9 shrink-0" /> <div className="flex gap-2">
<div className="line-clamp-1 text-start">{event.content}</div> <div className="w-9 shrink-0" />
</div> <div className="line-clamp-1 text-start">{event.content}</div>
</Note.Root> </div>
</Note.Provider> </Note.Root>
</button> </Note.Provider>
); );
} }
+61 -43
View File
@@ -6,9 +6,10 @@ import { ArrowRightCircleIcon } from "@lume/icons";
import { type LumeEvent, NostrQuery } from "@lume/system"; import { type LumeEvent, NostrQuery } from "@lume/system";
import { type ColumnRouteSearch, Kind } from "@lume/types"; import { type ColumnRouteSearch, Kind } from "@lume/types";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { useInfiniteQuery } from "@tanstack/react-query"; import { useInfiniteQuery } from "@tanstack/react-query";
import { createFileRoute, redirect } from "@tanstack/react-router"; import { createFileRoute, redirect } from "@tanstack/react-router";
import { useCallback } from "react"; import { useCallback, useRef } from "react";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
type Topic = { type Topic = {
@@ -71,6 +72,8 @@ export function Screen() {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}); });
const ref = useRef<HTMLDivElement>(null);
const renderItem = useCallback( const renderItem = useCallback(
(event: LumeEvent) => { (event: LumeEvent) => {
if (!event) return; if (!event) return;
@@ -94,48 +97,63 @@ export function Screen() {
); );
return ( return (
<div className="w-full h-full p-3 overflow-y-auto scrollbar-none"> <ScrollArea.Root
{isFetching && !isLoading && !isFetchingNextPage ? ( type={"scroll"}
<div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"> scrollHideDelay={300}
<div className="flex items-center justify-center gap-2"> className="overflow-hidden size-full"
<Spinner className="size-5" /> >
<span className="text-sm font-medium">Fetching new notes...</span> <ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
</div> <Virtualizer scrollRef={ref}>
</div> {isFetching && !isLoading && !isFetchingNextPage ? (
) : null} <div className="flex items-center justify-center w-full mb-3 h-11 bg-black/10 dark:bg-white/10 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50">
{isLoading ? ( <div className="flex items-center justify-center gap-2">
<div className="flex items-center justify-center w-full h-16 gap-2"> <Spinner className="size-5" />
<Spinner className="size-5" /> <span className="text-sm font-medium">
<span className="text-sm font-medium">Loading...</span> Fetching new notes...
</div> </span>
) : !data.length ? ( </div>
<div className="flex items-center justify-center"> </div>
Yo. You're catching up on all the things happening around you. ) : null}
</div> {isLoading ? (
) : ( <div className="flex items-center justify-center w-full h-16 gap-2">
<Virtualizer overscan={3}>
{data.map((item) => renderItem(item))}
</Virtualizer>
)}
{data?.length && hasNextPage ? (
<div>
<button
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
>
{isFetchingNextPage ? (
<Spinner className="size-5" /> <Spinner className="size-5" />
) : ( <span className="text-sm font-medium">Loading...</span>
<> </div>
<ArrowRightCircleIcon className="size-5" /> ) : !data.length ? (
Load more <div className="flex items-center justify-center">
</> Yo. You're catching up on all the things happening around you.
)} </div>
</button> ) : (
</div> data.map((item) => renderItem(item))
) : null} )}
</div> {data?.length && hasNextPage ? (
<div>
<button
type="button"
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage || isLoading}
className="inline-flex items-center justify-center w-full gap-2 px-3 font-medium h-9 rounded-xl bg-black/5 hover:bg-black/10 focus:outline-none dark:bg-white/10 dark:hover:bg-white/20"
>
{isFetchingNextPage ? (
<Spinner className="size-5" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
</div>
) : null}
</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
); );
} }
+41 -26
View File
@@ -2,9 +2,10 @@ import { TextNote } from "@/components/text";
import { LumeEvent } from "@lume/system"; import { LumeEvent } from "@lume/system";
import type { NostrEvent } from "@lume/types"; import type { NostrEvent } from "@lume/types";
import { Spinner } from "@lume/ui"; import { Spinner } from "@lume/ui";
import * as ScrollArea from "@radix-ui/react-scroll-area";
import { Await, createFileRoute } from "@tanstack/react-router"; import { Await, createFileRoute } from "@tanstack/react-router";
import { defer } from "@tanstack/react-router"; import { defer } from "@tanstack/react-router";
import { Suspense } from "react"; import { Suspense, useRef } from "react";
import { Virtualizer } from "virtua"; import { Virtualizer } from "virtua";
export const Route = createFileRoute("/trending/notes")({ export const Route = createFileRoute("/trending/notes")({
@@ -34,33 +35,47 @@ export const Route = createFileRoute("/trending/notes")({
export function Screen() { export function Screen() {
const { data } = Route.useLoaderData(); const { data } = Route.useLoaderData();
const ref = useRef<HTMLDivElement>(null);
return ( return (
<div className="w-full h-full"> <ScrollArea.Root
<Virtualizer overscan={3}> type={"scroll"}
<Suspense scrollHideDelay={300}
fallback={ className="overflow-hidden size-full"
<div className="flex flex-col items-center justify-center w-full h-20 gap-1"> >
<button <ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
type="button" <Virtualizer scrollRef={ref}>
className="inline-flex items-center gap-2 text-sm font-medium" <Suspense
disabled fallback={
> <div className="flex flex-col items-center justify-center w-full h-20 gap-1">
<Spinner className="size-5" /> <button
Loading... type="button"
</button> className="inline-flex items-center gap-2 text-sm font-medium"
</div> disabled
} >
> <Spinner className="size-5" />
<Await promise={data}> Loading...
{(notes) => </button>
notes.map((event) => ( </div>
<TextNote key={event.id} event={event} className="mb-3" />
))
} }
</Await> >
</Suspense> <Await promise={data}>
</Virtualizer> {(notes) =>
</div> notes.map((event) => (
<TextNote key={event.id} event={event} className="mb-3" />
))
}
</Await>
</Suspense>
</Virtualizer>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="flex select-none touch-none p-0.5 duration-[160ms] ease-out data-[orientation=vertical]:w-2"
orientation="vertical"
>
<ScrollArea.Thumb className="flex-1 bg-black/10 dark:bg-white/10 rounded-full relative before:content-[''] before:absolute before:top-1/2 before:left-1/2 before:-translate-x-1/2 before:-translate-y-1/2 before:w-full before:h-full before:min-w-[44px] before:min-h-[44px]" />
</ScrollArea.Scrollbar>
<ScrollArea.Corner className="bg-transparent" />
</ScrollArea.Root>
); );
} }
+11 -7
View File
@@ -1,17 +1,21 @@
import { Box, Container, Spinner } from "@lume/ui";
import { User } from "@/components/user";
import { createFileRoute, defer } from "@tanstack/react-router";
import { WindowVirtualizer } from "virtua";
import { Conversation } from "@/components/conversation"; import { Conversation } from "@/components/conversation";
import { Quote } from "@/components/quote"; import { Quote } from "@/components/quote";
import { RepostNote } from "@/components/repost"; import { RepostNote } from "@/components/repost";
import { TextNote } from "@/components/text"; import { TextNote } from "@/components/text";
import { Kind } from "@lume/types"; import { User } from "@/components/user";
import { Suspense, useCallback } from "react";
import { Await } from "@tanstack/react-router";
import { type LumeEvent, NostrQuery } from "@lume/system"; import { type LumeEvent, NostrQuery } from "@lume/system";
import { Kind } from "@lume/types";
import { Box, Container, Spinner } from "@lume/ui";
import { createFileRoute, defer } from "@tanstack/react-router";
import { Await } from "@tanstack/react-router";
import { Suspense, useCallback } from "react";
import { WindowVirtualizer } from "virtua";
export const Route = createFileRoute("/users/$pubkey")({ export const Route = createFileRoute("/users/$pubkey")({
beforeLoad: async () => {
const settings = await NostrQuery.getUserSettings();
return { settings };
},
loader: async ({ params }) => { loader: async ({ params }) => {
return { data: defer(NostrQuery.getUserEvents(params.pubkey)) }; return { data: defer(NostrQuery.getUserEvents(params.pubkey)) };
}, },
+13 -7
View File
@@ -17,6 +17,7 @@ export class LumeEvent {
public sig: string; public sig: string;
public meta: Meta; public meta: Meta;
public relay?: string; public relay?: string;
public replies?: LumeEvent[];
#raw: NostrEvent; #raw: NostrEvent;
constructor(event: NostrEvent) { constructor(event: NostrEvent) {
@@ -94,20 +95,22 @@ export class LumeEvent {
return { id, relayHint }; return { id, relayHint };
} }
public async getReplies(id: string) { public async getAllReplies() {
const query = await commands.getReplies(id); const query = await commands.getReplies(this.id);
if (query.status === "ok") { if (query.status === "ok") {
const events = query.data.map((item) => { const events = query.data.map((item) => {
const raw = JSON.parse(item.raw) as EventWithReplies; const nostrEvent: NostrEvent = JSON.parse(item.raw);
if (item.parsed) { if (item.parsed) {
raw.meta = item.parsed; nostrEvent.meta = item.parsed;
} else { } else {
raw.meta = null; nostrEvent.meta = null;
} }
return raw; const lumeEvent = new LumeEvent(nostrEvent);
return lumeEvent;
}); });
if (events.length > 0) { if (events.length > 0) {
@@ -115,7 +118,7 @@ export class LumeEvent {
for (const event of events) { for (const event of events) {
const tags = event.tags.filter( const tags = event.tags.filter(
(el) => el[0] === "e" && el[1] !== id && el[3] !== "mention", (el) => el[0] === "e" && el[1] !== this.id && el[3] !== "mention",
); );
if (tags.length > 0) { if (tags.length > 0) {
@@ -141,6 +144,9 @@ export class LumeEvent {
} }
return events; return events;
} else {
console.error(query.error);
return [];
} }
} }
+1 -1
View File
@@ -10,7 +10,7 @@ import { LumeEvent } from "./event";
export class NostrQuery { export class NostrQuery {
static #toLumeEvents(richEvents: RichEvent[]) { static #toLumeEvents(richEvents: RichEvent[]) {
const events = richEvents.map((item) => { const events = richEvents.map((item) => {
const nostrEvent = JSON.parse(item.raw) as NostrEvent; const nostrEvent: NostrEvent = JSON.parse(item.raw);
if (item.parsed) { if (item.parsed) {
nostrEvent.meta = item.parsed; nostrEvent.meta = item.parsed;
+19 -21
View File
@@ -11,29 +11,25 @@ interface CarouselProps<T> {
interface CarouselRenderItemProps<T> { interface CarouselRenderItemProps<T> {
readonly item: T; readonly item: T;
readonly index: number;
readonly isSnapPoint: boolean; readonly isSnapPoint: boolean;
} }
export const Carousel = <T,>({ items, renderItem }: CarouselProps<T>) => { export const Carousel = <T,>({ items, renderItem }: CarouselProps<T>) => {
const { const { scrollRef, pages, activePageIndex, prev, next, snapPointIndexes } =
scrollRef, useSnapCarousel();
pages,
activePageIndex,
prev,
next,
goTo,
snapPointIndexes,
} = useSnapCarousel();
return ( return (
<div className="relative group"> <div className="relative group">
<ul <ul
ref={scrollRef} ref={scrollRef}
className="relative flex overflow-auto snap-x scrollbar-none" className="relative flex overflow-auto snap-x scrollbar-none"
> >
{items.map((item, i) => {items.map((item, index) =>
renderItem({ renderItem({
item, item,
isSnapPoint: snapPointIndexes.has(i), index,
isSnapPoint: snapPointIndexes.has(index),
}), }),
)} )}
</ul> </ul>
@@ -74,13 +70,15 @@ interface CarouselItemProps {
readonly children?: React.ReactNode; readonly children?: React.ReactNode;
} }
export const CarouselItem = ({ isSnapPoint, children }: CarouselItemProps) => ( export const CarouselItem = ({ isSnapPoint, children }: CarouselItemProps) => {
<li return (
className={cn( <li
"w-[240px] h-[320px] shrink-0 pl-3 last:pr-2", className={cn(
isSnapPoint ? "" : "snap-start", "w-[240px] h-[320px] shrink-0 pl-3 last:pr-2",
)} isSnapPoint ? "" : "snap-start",
> )}
{children} >
</li> {children}
); </li>
);
};
+159
View File
@@ -87,6 +87,9 @@ importers:
'@radix-ui/react-popover': '@radix-ui/react-popover':
specifier: ^1.0.7 specifier: ^1.0.7
version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-scroll-area':
specifier: ^1.1.0
version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-switch': '@radix-ui/react-switch':
specifier: ^1.0.3 specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
@@ -1498,12 +1501,20 @@ packages:
requiresBuild: true requiresBuild: true
optional: true optional: true
/@radix-ui/number@1.1.0:
resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==}
dev: false
/@radix-ui/primitive@1.0.1: /@radix-ui/primitive@1.0.1:
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
dependencies: dependencies:
'@babel/runtime': 7.24.7 '@babel/runtime': 7.24.7
dev: false dev: false
/@radix-ui/primitive@1.1.0:
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
dev: false
/@radix-ui/react-arrow@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==} resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==}
peerDependencies: peerDependencies:
@@ -1643,6 +1654,19 @@ packages:
react: 18.3.1 react: 18.3.1
dev: false dev: false
/@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@18.3.1): /@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
peerDependencies: peerDependencies:
@@ -1657,6 +1681,19 @@ packages:
react: 18.3.1 react: 18.3.1
dev: false dev: false
/@radix-ui/react-context@1.1.0(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): /@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==} resolution: {integrity: sha512-GjWJX/AUpB703eEBanuBnIWdIXg6NvJFCXcNlSZk4xdszCdhrJgBoUd1cGk67vFO+WdA2pfI/plOpqz/5GUP6Q==}
peerDependencies: peerDependencies:
@@ -1705,6 +1742,19 @@ packages:
react: 18.3.1 react: 18.3.1
dev: false dev: false
/@radix-ui/react-direction@1.1.0(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): /@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==} resolution: {integrity: sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g==}
peerDependencies: peerDependencies:
@@ -1984,6 +2034,27 @@ packages:
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
dev: false dev: false
/@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): /@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==} resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
peerDependencies: peerDependencies:
@@ -2005,6 +2076,26 @@ packages:
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
dev: false dev: false
/@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): /@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==} resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==}
peerDependencies: peerDependencies:
@@ -2034,6 +2125,34 @@ packages:
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
dev: false dev: false
/@radix-ui/react-scroll-area@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-9ArIZ9HWhsrfqS765h+GZuLoxaRHD/j0ZWOWilsCvYTpYJp8XwCqNG7Dt9Nu/TItKOdgLGkOPCodQvDc+UMwYg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@radix-ui/number': 1.1.0
'@radix-ui/primitive': 1.1.0
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-direction': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@18.3.1): /@radix-ui/react-slot@1.0.2(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
peerDependencies: peerDependencies:
@@ -2049,6 +2168,20 @@ packages:
react: 18.3.1 react: 18.3.1
dev: false dev: false
/@radix-ui/react-slot@1.1.0(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-switch@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): /@radix-ui/react-switch@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==} resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==}
peerDependencies: peerDependencies:
@@ -2150,6 +2283,19 @@ packages:
react: 18.3.1 react: 18.3.1
dev: false dev: false
/@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.3)(react@18.3.1): /@radix-ui/react-use-controllable-state@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
peerDependencies: peerDependencies:
@@ -2194,6 +2340,19 @@ packages:
react: 18.3.1 react: 18.3.1
dev: false dev: false
/@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.3.3
react: 18.3.1
dev: false
/@radix-ui/react-use-previous@1.0.1(@types/react@18.3.3)(react@18.3.1): /@radix-ui/react-use-previous@1.0.1(@types/react@18.3.3)(react@18.3.1):
resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==}
peerDependencies: peerDependencies:
+17 -16
View File
@@ -1,5 +1,6 @@
use cocoa::appkit::NSWindowCollectionBehavior;
use std::ffi::CString; use std::ffi::CString;
use cocoa::appkit::NSWindowCollectionBehavior;
use tauri::Manager; use tauri::Manager;
use tauri_nspanel::{ use tauri_nspanel::{
block::ConcreteBlock, block::ConcreteBlock,
@@ -8,44 +9,44 @@ use tauri_nspanel::{
base::{id, nil}, base::{id, nil},
foundation::{NSPoint, NSRect}, foundation::{NSPoint, NSRect},
}, },
objc::{class, msg_send, runtime::NO, sel, sel_impl}, ManagerExt,
panel_delegate, ManagerExt, WebviewWindowExt, objc::{class, msg_send, runtime::NO, sel, sel_impl}, panel_delegate, WebviewWindowExt,
}; };
#[allow(non_upper_case_globals)] #[allow(non_upper_case_globals)]
const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7; const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7;
pub fn swizzle_to_menubar_panel(app_handle: &tauri::AppHandle) { pub fn swizzle_to_menubar_panel(app_handle: &tauri::AppHandle) {
let window = app_handle.get_webview_window("panel").unwrap(); let panel_delegate = panel_delegate!(SpotlightPanelDelegate {
let panel = window.to_panel().unwrap();
let handle = app_handle.to_owned();
let delegate = panel_delegate!(MyPanelDelegate {
window_did_become_key,
window_did_resign_key window_did_resign_key
}); });
delegate.set_listener(Box::new(move |delegate_name: String| { let window = app_handle.get_webview_window("panel").unwrap();
let panel = window.to_panel().unwrap();
let handle = app_handle.clone();
panel_delegate.set_listener(Box::new(move |delegate_name: String| {
match delegate_name.as_str() { match delegate_name.as_str() {
"window_did_become_key" => {
let app_name = handle.package_info().name.to_owned();
println!("[info]: {:?} panel becomes key window!", app_name);
}
"window_did_resign_key" => { "window_did_resign_key" => {
println!("[info]: panel resigned from key window!"); let _ = handle.emit("menubar_panel_did_resign_key", ());
} }
_ => (), _ => (),
} }
})); }));
panel.set_level(NSMainMenuWindowLevel + 1); panel.set_level(NSMainMenuWindowLevel + 1);
panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel); panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel);
panel.set_collection_behaviour( panel.set_collection_behaviour(
NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary | NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary, | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary,
); );
panel.set_delegate(delegate);
panel.set_delegate(panel_delegate);
} }
pub fn setup_menubar_panel_listeners(app_handle: &tauri::AppHandle) { pub fn setup_menubar_panel_listeners(app_handle: &tauri::AppHandle) {
+7 -7
View File
@@ -9,20 +9,20 @@ extern crate cocoa;
#[macro_use] #[macro_use]
extern crate objc; extern crate objc;
use std::sync::Mutex;
use std::time::Duration;
use std::{ use std::{
fs, fs,
io::{self, BufRead}, io::{self, BufRead},
str::FromStr, str::FromStr,
}; };
use std::sync::Mutex;
use std::time::Duration;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type; use specta::Type;
use tauri::{Manager, path::BaseDirectory};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use tauri::tray::{MouseButtonState, TrayIconEvent}; use tauri::tray::{MouseButtonState, TrayIconEvent};
use tauri::{path::BaseDirectory, Manager};
use tauri_nspanel::ManagerExt; use tauri_nspanel::ManagerExt;
use tauri_plugin_decorum::WebviewWindowExt; use tauri_plugin_decorum::WebviewWindowExt;
@@ -152,11 +152,11 @@ fn main() {
// Create panel // Create panel
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
swizzle_to_menubar_panel(&app.handle()); swizzle_to_menubar_panel(app.handle());
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
update_menubar_appearance(&app.handle()); update_menubar_appearance(app.handle());
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
setup_menubar_panel_listeners(&app.handle()); setup_menubar_panel_listeners(app.handle());
// Setup tray icon // Setup tray icon
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -173,7 +173,7 @@ fn main() {
match panel.is_visible() { match panel.is_visible() {
true => panel.order_out(None), true => panel.order_out(None),
false => { false => {
position_menubar_panel(&app, 0.0); position_menubar_panel(app, 0.0);
panel.show(); panel.show();
} }
} }
+2 -2
View File
@@ -99,10 +99,10 @@ pub async fn get_event_from(
Ok(RichEvent { raw, parsed }) Ok(RichEvent { raw, parsed })
} else { } else {
return Err("Cannot found this event with current relay list".into()); Err("Cannot found this event with current relay list".into())
} }
} }
Err(err) => return Err(err.to_string()), Err(err) => Err(err.to_string()),
} }
} else { } else {
// Add relay hint to relay pool // Add relay hint to relay pool
+14 -17
View File
@@ -64,20 +64,14 @@ pub async fn save_account(
password: &str, password: &str,
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<String, String> { ) -> Result<String, String> {
let secret_key: Result<SecretKey, String>; let secret_key = if nsec.starts_with("ncryptsec") {
if nsec.starts_with("ncryptsec") {
let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap(); let encrypted_key = EncryptedSecretKey::from_bech32(nsec).unwrap();
secret_key = match encrypted_key.to_secret_key(password) { encrypted_key
Ok(val) => Ok(val), .to_secret_key(password)
Err(err) => Err(err.to_string()), .map_err(|err| err.to_string())
};
} else { } else {
secret_key = match SecretKey::from_bech32(nsec) { SecretKey::from_bech32(nsec).map_err(|err| err.to_string())
Ok(val) => Ok(val), };
Err(err) => Err(err.to_string()),
}
}
match secret_key { match secret_key {
Ok(val) => { Ok(val) => {
@@ -280,11 +274,14 @@ pub async fn load_account(
if subscription_id == notification_id { if subscription_id == notification_id {
println!("new notification: {}", event.as_json()); println!("new notification: {}", event.as_json());
if let Err(_) = app.emit_to( if app
EventTarget::window("panel"), .emit_to(
"notification", EventTarget::window("panel"),
event.as_json(), "notification",
) { event.as_json(),
)
.is_err()
{
println!("Emit new notification failed.") println!("Emit new notification failed.")
} }
+3 -3
View File
@@ -117,12 +117,12 @@ pub fn get_bootstrap_relays(app: tauri::AppHandle) -> Result<Vec<String>, ()> {
.resolve("resources/relays.txt", BaseDirectory::Resource) .resolve("resources/relays.txt", BaseDirectory::Resource)
.expect("Bootstrap relays not found."); .expect("Bootstrap relays not found.");
let file = std::fs::File::open(&relays_path).unwrap(); let file = std::fs::File::open(relays_path).unwrap();
let lines = io::BufReader::new(file).lines(); let lines = io::BufReader::new(file).lines();
let mut relays = Vec::new(); let mut relays = Vec::new();
for line in lines.flatten() { for line in lines.map_while(Result::ok) {
relays.push(line.to_string()) relays.push(line.to_string())
} }
@@ -139,7 +139,7 @@ pub fn save_bootstrap_relays(relays: &str, app: tauri::AppHandle) -> Result<(),
let mut file = fs::OpenOptions::new() let mut file = fs::OpenOptions::new()
.write(true) .write(true)
.open(&relays_path) .open(relays_path)
.unwrap(); .unwrap();
match file.write_all(relays.as_bytes()) { match file.write_all(relays.as_bytes()) {
+2 -2
View File
@@ -2,8 +2,8 @@ use std::collections::HashSet;
use std::str::FromStr; use std::str::FromStr;
use linkify::LinkFinder; use linkify::LinkFinder;
use nostr_sdk::{Alphabet, Event, EventId, FromBech32, PublicKey, SingleLetterTag, Tag, TagKind};
use nostr_sdk::prelude::Nip19Event; use nostr_sdk::prelude::Nip19Event;
use nostr_sdk::{Alphabet, Event, EventId, FromBech32, PublicKey, SingleLetterTag, Tag, TagKind};
use reqwest::Client; use reqwest::Client;
use serde::Serialize; use serde::Serialize;
use specta::Type; use specta::Type;
@@ -176,7 +176,7 @@ pub fn create_event_tags(content: &str) -> Vec<Tag> {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for mention in mentions { for mention in mentions {
let entity = mention.replace("nostr:", "").replace("@", ""); let entity = mention.replace("nostr:", "").replace('@', "");
if !tag_set.contains(&entity) { if !tag_set.contains(&entity) {
if entity.starts_with("npub") { if entity.starts_with("npub") {
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "../node_modules/@tauri-apps/cli/schema.json", "$schema": "../node_modules/@tauri-apps/cli/schema.json",
"productName": "Lume", "productName": "Lume",
"version": "4.0.10", "version": "4.0.11",
"identifier": "nu.lume.Lume", "identifier": "nu.lume.Lume",
"build": { "build": {
"beforeBuildCommand": "pnpm desktop:build", "beforeBuildCommand": "pnpm desktop:build",