Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4a317f038 | ||
|
|
9779d020c7 | ||
|
|
f8280ec8ee | ||
|
|
6c26f8967b |
@@ -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",
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -1,57 +1,48 @@
|
|||||||
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" />
|
useEffect(() => {
|
||||||
</div>
|
let mounted = true;
|
||||||
);
|
|
||||||
|
if (event) {
|
||||||
|
event.getAllReplies().then((data) => {
|
||||||
|
if (mounted) setReplies(data);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
return () => {
|
||||||
<div className="flex items-center justify-center w-full h-full">
|
mounted = false;
|
||||||
<p>Not found.</p>
|
};
|
||||||
</div>;
|
}, [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 ? (
|
|
||||||
<ReplyList eventId={eventId} />
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center justify-center w-full h-full">
|
|
||||||
<Spinner className="size-5" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</WindowVirtualizer>
|
|
||||||
</Box>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MainNote({ data }: { data: LumeEvent }) {
|
|
||||||
return (
|
|
||||||
<Note.Provider event={data}>
|
|
||||||
<Note.Root>
|
<Note.Root>
|
||||||
<div className="flex items-center justify-between px-3 h-14">
|
<div className="flex items-center justify-between px-3 h-14">
|
||||||
<Note.User />
|
<Note.User />
|
||||||
@@ -65,5 +56,27 @@ function MainNote({ data }: { data: LumeEvent }) {
|
|||||||
</div>
|
</div>
|
||||||
</Note.Root>
|
</Note.Root>
|
||||||
</Note.Provider>
|
</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>
|
||||||
|
{!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>
|
||||||
|
</Box>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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,12 +73,20 @@ export function Screen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full p-3 overflow-y-auto scrollbar-none">
|
<ScrollArea.Root
|
||||||
|
type={"scroll"}
|
||||||
|
scrollHideDelay={300}
|
||||||
|
className="overflow-hidden size-full"
|
||||||
|
>
|
||||||
|
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
|
||||||
|
<Virtualizer scrollRef={ref}>
|
||||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||||
<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">
|
<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">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
<span className="text-sm font-medium">Fetching new notes...</span>
|
<span className="text-sm font-medium">
|
||||||
|
Fetching new notes...
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -89,9 +100,7 @@ export function Screen() {
|
|||||||
Yo. You're catching up on all the things happening around you.
|
Yo. You're catching up on all the things happening around you.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Virtualizer overscan={3}>
|
data.map((item) => renderItem(item))
|
||||||
{data.map((item) => renderItem(item))}
|
|
||||||
</Virtualizer>
|
|
||||||
)}
|
)}
|
||||||
{data?.length && hasNextPage ? (
|
{data?.length && hasNextPage ? (
|
||||||
<div>
|
<div>
|
||||||
@@ -112,6 +121,15 @@ export function Screen() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +87,20 @@ export function Screen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full p-3 overflow-y-auto scrollbar-none">
|
<ScrollArea.Root
|
||||||
|
type={"scroll"}
|
||||||
|
scrollHideDelay={300}
|
||||||
|
className="overflow-hidden size-full"
|
||||||
|
>
|
||||||
|
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
|
||||||
|
<Virtualizer scrollRef={ref}>
|
||||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||||
<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">
|
<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">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
<span className="text-sm font-medium">Fetching new notes...</span>
|
<span className="text-sm font-medium">
|
||||||
|
Fetching new notes...
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -103,9 +114,7 @@ export function Screen() {
|
|||||||
Yo. You're catching up on all the things happening around you.
|
Yo. You're catching up on all the things happening around you.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Virtualizer overscan={3}>
|
data.map((item) => renderItem(item))
|
||||||
{data.map((item) => renderItem(item))}
|
|
||||||
</Virtualizer>
|
|
||||||
)}
|
)}
|
||||||
{data?.length && hasNextPage ? (
|
{data?.length && hasNextPage ? (
|
||||||
<div>
|
<div>
|
||||||
@@ -126,6 +135,15 @@ export function Screen() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +85,20 @@ export function Screen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full p-3 overflow-y-auto scrollbar-none">
|
<ScrollArea.Root
|
||||||
|
type={"scroll"}
|
||||||
|
scrollHideDelay={300}
|
||||||
|
className="overflow-hidden size-full"
|
||||||
|
>
|
||||||
|
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
|
||||||
|
<Virtualizer scrollRef={ref}>
|
||||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||||
<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">
|
<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">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
<span className="text-sm font-medium">Fetching new notes...</span>
|
<span className="text-sm font-medium">
|
||||||
|
Fetching new notes...
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -101,9 +112,7 @@ export function Screen() {
|
|||||||
Yo. You're catching up on all the things happening around you.
|
Yo. You're catching up on all the things happening around you.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Virtualizer overscan={3}>
|
data.map((item) => renderItem(item))
|
||||||
{data.map((item) => renderItem(item))}
|
|
||||||
</Virtualizer>
|
|
||||||
)}
|
)}
|
||||||
{data?.length && hasNextPage ? (
|
{data?.length && hasNextPage ? (
|
||||||
<div>
|
<div>
|
||||||
@@ -124,6 +133,15 @@ export function Screen() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,13 +367,8 @@ function TextNote({ event }: { event: LumeEvent }) {
|
|||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
key={event.id}
|
|
||||||
onClick={() => LumeWindow.openEvent(event)}
|
|
||||||
>
|
|
||||||
<Note.Provider event={event}>
|
<Note.Provider event={event}>
|
||||||
<Note.Root className="flex flex-col p-2 rounded-lg shrink-0 backdrop-blur-md bg-black/10 dark:bg-white/10">
|
<Note.Root className="flex flex-col p-2 mb-2 rounded-lg shrink-0 backdrop-blur-md bg-black/10 dark:bg-white/10">
|
||||||
<User.Provider pubkey={event.pubkey}>
|
<User.Provider pubkey={event.pubkey}>
|
||||||
<User.Root className="inline-flex items-center gap-2">
|
<User.Root className="inline-flex items-center gap-2">
|
||||||
<User.Avatar className="rounded-full size-9 shrink-0" />
|
<User.Avatar className="rounded-full size-9 shrink-0" />
|
||||||
@@ -372,6 +402,5 @@ function TextNote({ event }: { event: LumeEvent }) {
|
|||||||
</div>
|
</div>
|
||||||
</Note.Root>
|
</Note.Root>
|
||||||
</Note.Provider>
|
</Note.Provider>
|
||||||
</button>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,12 +97,20 @@ export function Screen() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full p-3 overflow-y-auto scrollbar-none">
|
<ScrollArea.Root
|
||||||
|
type={"scroll"}
|
||||||
|
scrollHideDelay={300}
|
||||||
|
className="overflow-hidden size-full"
|
||||||
|
>
|
||||||
|
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
|
||||||
|
<Virtualizer scrollRef={ref}>
|
||||||
{isFetching && !isLoading && !isFetchingNextPage ? (
|
{isFetching && !isLoading && !isFetchingNextPage ? (
|
||||||
<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">
|
<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">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<Spinner className="size-5" />
|
<Spinner className="size-5" />
|
||||||
<span className="text-sm font-medium">Fetching new notes...</span>
|
<span className="text-sm font-medium">
|
||||||
|
Fetching new notes...
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -113,9 +124,7 @@ export function Screen() {
|
|||||||
Yo. You're catching up on all the things happening around you.
|
Yo. You're catching up on all the things happening around you.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Virtualizer overscan={3}>
|
data.map((item) => renderItem(item))
|
||||||
{data.map((item) => renderItem(item))}
|
|
||||||
</Virtualizer>
|
|
||||||
)}
|
)}
|
||||||
{data?.length && hasNextPage ? (
|
{data?.length && hasNextPage ? (
|
||||||
<div>
|
<div>
|
||||||
@@ -136,6 +145,15 @@ export function Screen() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,10 +35,16 @@ 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"}
|
||||||
|
scrollHideDelay={300}
|
||||||
|
className="overflow-hidden size-full"
|
||||||
|
>
|
||||||
|
<ScrollArea.Viewport ref={ref} className="h-full px-3 pb-3">
|
||||||
|
<Virtualizer scrollRef={ref}>
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<div className="flex flex-col items-center justify-center w-full h-20 gap-1">
|
<div className="flex flex-col items-center justify-center w-full h-20 gap-1">
|
||||||
@@ -61,6 +68,14 @@ export function Screen() {
|
|||||||
</Await>
|
</Await>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Virtualizer>
|
</Virtualizer>
|
||||||
</div>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)) };
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,7 +70,8 @@ interface CarouselItemProps {
|
|||||||
readonly children?: React.ReactNode;
|
readonly children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CarouselItem = ({ isSnapPoint, children }: CarouselItemProps) => (
|
export const CarouselItem = ({ isSnapPoint, children }: CarouselItemProps) => {
|
||||||
|
return (
|
||||||
<li
|
<li
|
||||||
className={cn(
|
className={cn(
|
||||||
"w-[240px] h-[320px] shrink-0 pl-3 last:pr-2",
|
"w-[240px] h-[320px] shrink-0 pl-3 last:pr-2",
|
||||||
@@ -83,4 +80,5 @@ export const CarouselItem = ({ isSnapPoint, children }: CarouselItemProps) => (
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
Generated
+159
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+11
-14
@@ -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
|
||||||
|
.emit_to(
|
||||||
EventTarget::window("panel"),
|
EventTarget::window("panel"),
|
||||||
"notification",
|
"notification",
|
||||||
event.as_json(),
|
event.as_json(),
|
||||||
) {
|
)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
println!("Emit new notification failed.")
|
println!("Emit new notification failed.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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,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",
|
||||||
|
|||||||
Reference in New Issue
Block a user