chore: clean up

This commit is contained in:
reya 2024-03-16 18:45:54 +07:00
parent 46cc01e0ee
commit c8e014f33e
67 changed files with 163 additions and 2723 deletions

View File

@ -29,10 +29,10 @@
"react-currency-input-field": "^3.8.0", "react-currency-input-field": "^3.8.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-i18next": "^14.1.0", "react-i18next": "^14.1.0",
"slate": "^0.101.5", "slate": "^0.102.0",
"slate-react": "^0.101.6", "slate-react": "^0.102.0",
"sonner": "^1.4.3", "sonner": "^1.4.3",
"virtua": "^0.27.5" "virtua": "^0.29.0"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",

View File

@ -0,0 +1,50 @@
import { LoaderIcon } from "@lume/icons";
import { Column } from "@lume/ui";
import { createFileRoute } from "@tanstack/react-router";
import { useState } from "react";
export const Route = createFileRoute("/$account/home")({
component: Screen,
pendingComponent: Pending,
loader: async () => {
const columns = [
{ name: "Tauri v2", content: "https://beta.tauri.app" },
{ name: "Tauri v1", content: "https://tauri.app" },
{ name: "Lume", content: "https://lume.nu" },
{ name: "Snort", content: "https://snort.social" },
];
return columns;
},
});
function Screen() {
const data = Route.useLoaderData();
const [isScroll, setIsScroll] = useState(false);
return (
<div className="relative h-full w-full">
<div
onScroll={() => setIsScroll((state) => !state)}
className="flex h-full w-full flex-nowrap gap-3 overflow-x-auto px-3 pb-3 pt-1.5 focus:outline-none"
>
{data.map((column, index) => (
<Column
key={column.name + index}
column={column}
isScroll={isScroll}
/>
))}
</div>
</div>
);
}
function Pending() {
return (
<div className="flex h-full w-full items-center justify-center">
<button type="button" disabled>
<LoaderIcon className="size-5 animate-spin" />
</button>
</div>
);
}

View File

@ -1,36 +0,0 @@
import { LumeColumn } from "@lume/types";
import { Column } from "@lume/ui";
import { createLazyFileRoute } from "@tanstack/react-router";
import { useState } from "react";
const DEFAULT_COLUMNS: LumeColumn[] = [
{ name: "Tauri v2", content: "https://beta.tauri.app" },
{ name: "Tauri v1", content: "https://tauri.app" },
{ name: "Lume", content: "https://lume.nu" },
{ name: "Snort", content: "https://snort.social" },
];
export const Route = createLazyFileRoute("/$account/home")({
component: Screen,
});
function Screen() {
const [isScroll, setIsScroll] = useState(false);
return (
<div className="relative h-full w-full">
<div
onScroll={() => setIsScroll((state) => !state)}
className="flex h-full w-full flex-nowrap gap-3 overflow-x-auto px-3 pb-3 pt-1.5 focus:outline-none"
>
{DEFAULT_COLUMNS.map((column, index) => (
<Column
key={column.name + index}
column={column}
isScroll={isScroll}
/>
))}
</div>
</div>
);
}

View File

@ -10,7 +10,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.4.1", "@astrojs/check": "^0.5.9",
"@astrojs/tailwind": "^5.1.0", "@astrojs/tailwind": "^5.1.0",
"@fontsource/geist-mono": "^5.0.2", "@fontsource/geist-mono": "^5.0.2",
"astro": "^4.5.5", "astro": "^4.5.5",

View File

@ -17,7 +17,7 @@
"@tanstack/react-query": "^5.28.4", "@tanstack/react-query": "^5.28.4",
"@tanstack/react-router": "^1.20.0", "@tanstack/react-router": "^1.20.0",
"get-urls": "^12.1.0", "get-urls": "^12.1.0",
"media-chrome": "^2.2.5", "media-chrome": "^3.0.2",
"minidenticons": "^4.2.1", "minidenticons": "^4.2.1",
"nanoid": "^5.0.6", "nanoid": "^5.0.6",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
@ -28,7 +28,7 @@
"react-string-replace": "^1.1.1", "react-string-replace": "^1.1.1",
"sonner": "^1.4.3", "sonner": "^1.4.3",
"string-strip-html": "^13.4.6", "string-strip-html": "^13.4.6",
"virtua": "^0.27.5" "virtua": "^0.29.0"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",

View File

@ -0,0 +1,48 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import { useArk } from "./useArk";
const FETCH_LIMIT = 20;
const QUERY_KEY = "local";
const DEDUP = true;
export function useEvents(key: string, account?: string) {
const ark = useArk();
const {
data,
isError,
isLoading,
isRefetching,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
} = useInfiniteQuery({
queryKey: [key, account],
initialPageParam: 0,
queryFn: async ({ pageParam }: { pageParam: number }) => {
const events = await ark.get_events(
QUERY_KEY,
FETCH_LIMIT,
pageParam,
DEDUP,
);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage?.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
});
return {
data,
isError,
isLoading,
isRefetching,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
};
}

View File

@ -1,24 +0,0 @@
{
"name": "@columns/antenas",
"version": "0.0.0",
"private": true,
"main": "./src/index.tsx",
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.28.4",
"react": "^18.2.0",
"sonner": "^1.4.3",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.66",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2"
}
}

View File

@ -1,131 +0,0 @@
import { useColumnContext } from "@lume/ark";
import { CancelIcon, PlusIcon } from "@lume/icons";
import { useState } from "react";
import { toast } from "sonner";
export function AntenasForm({ id }: { id: number }) {
const { updateColumn, removeColumn } = useColumnContext();
const [title, setTitle] = useState<string>(`Antena-${id}`);
const [source, setSource] = useState("contacts");
const [hashtag, setHashtag] = useState("");
const [hashtags, setHashtags] = useState<string[]>([]);
const addHashtag = () => {
if (!hashtag.startsWith("#"))
return toast.error("Hashtag need to start with #");
if (hashtag.length > 64) return toast.error("Hashtag too long");
setHashtags((prev) => [...prev, hashtag]);
setHashtag("");
};
const removeHashtag = (item: string) => {
setHashtags((prev) => prev.filter((tag) => tag !== item));
};
const submit = async () => {
const content = {
hashtags,
source,
};
await updateColumn(id, title, JSON.stringify(content));
};
return (
<div className="flex flex-col justify-between h-full">
<div className="flex flex-col flex-1 min-h-0">
<div className="flex items-center justify-between w-full px-3 border-b h-11 shrink-0 border-neutral-100 dark:border-neutral-900">
<h1 className="text-sm font-semibold">Create a new Antena</h1>
<button
type="button"
onClick={async () => await removeColumn(id)}
className="inline-flex items-center justify-center rounded size-6 hover:bg-neutral-100 dark:hover:bg-neutral-900"
>
<CancelIcon className="size-4" />
</button>
</div>
<div className="flex flex-col h-full gap-5 px-3 pt-2 overflow-y-auto">
<div className="flex flex-col gap-1">
<label htmlFor="name" className="font-medium">
Name
</label>
<input
type="text"
name="name"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Nostrichs..."
className="px-2 border border-neutral-100 dark:border-neutral-900 bg-neutral-50 rounded-lg h-10 dark:bg-neutral-950 placeholder:text-neutral-600 focus:border-blue-500 focus:shadow-none focus:ring-0"
/>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="source"
className="select-none text-neutral-950 data-[disabled]:opacity-50 font-medium mb-1 dark:text-white"
>
Source
</label>
<select
name="source"
value={source}
onChange={(e) => setSource(e.target.value)}
className="px-2 w-full border border-neutral-100 dark:border-neutral-900 bg-neutral-50 rounded-lg dark:bg-neutral-950 placeholder:text-neutral-600 focus:border-blue-500 focus:shadow-none focus:ring-0"
>
<option value="contacts">Contacts</option>
<option value="global">Global</option>
</select>
</div>
<div className="flex flex-col gap-1">
<label
htmlFor="name"
className="select-none text-neutral-950 data-[disabled]:opacity-50 font-medium mb-1 dark:text-white"
>
Hashtags to listen to
</label>
<div className="flex items-center justify-between gap-2 mb-1">
<input
name="name"
value={hashtag}
onChange={(e) => setHashtag(e.target.value)}
onKeyPress={(event) => {
if (event.key === "Enter") addHashtag();
}}
placeholder="#nostr..."
className="px-2 w-full border border-neutral-100 dark:border-neutral-900 bg-neutral-50 rounded-lg h-10 dark:bg-neutral-950 placeholder:text-neutral-600 focus:border-blue-500 focus:shadow-none focus:ring-0"
/>
<button
type="button"
onClick={() => addHashtag()}
className="inline-flex items-center justify-center h-full text-white bg-blue-500 rounded-lg aspect-square shrink-0 hover:bg-blue-600"
>
<PlusIcon className="size-4" />
</button>
</div>
<div className="flex flex-wrap items-center justify-start gap-2">
{hashtags.map((item) => (
<button
key={item}
type="button"
onClick={() => removeHashtag(item)}
className="inline-flex items-center justify-center h-6 px-2 text-sm rounded-md w-min bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
>
{item}
</button>
))}
</div>
</div>
<div>
<button
type="button"
onClick={submit}
disabled={hashtags.length < 1}
className="w-full inline-flex items-center justify-center h-10 px-4 font-semibold text-white transform bg-blue-500 rounded-lg active:translate-y-1 hover:bg-blue-600 focus:outline-none disabled:opacity-50"
>
Create
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,132 +0,0 @@
import { RepostNote, TextNote, useArk } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKFilter, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react";
import { CacheSnapshot, VList, VListHandle } from "virtua";
export function HomeRoute({
colKey,
content,
}: { colKey: string; content: string }) {
const ark = useArk();
const ref = useRef<VListHandle>();
const cacheKey = `${colKey}-vlist`;
const [offset, cache] = useMemo(() => {
const serialized = sessionStorage.getItem(cacheKey);
if (!serialized) return [];
return JSON.parse(serialized) as [number, CacheSnapshot];
}, []);
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: [colKey],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
let filter: NDKFilter;
const parsed: { hashtags: string[]; source: string } =
JSON.parse(content);
if (parsed.source === "contacts") {
filter = {
kinds: [NDKKind.Text, NDKKind.Repost],
"#t": parsed.hashtags.map((item) => item.replace("#", "")),
authors: ark.account.contacts,
};
} else {
filter = {
kinds: [NDKKind.Text, NDKKind.Repost],
"#t": parsed.hashtags.map((item) => item.replace("#", "")),
};
}
const events = await ark.getInfiniteEvents({
filter,
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
refetchOnWindowFocus: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data],
);
const renderItem = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote key={event.id} event={event} className="mt-3" />;
case NDKKind.Repost:
return <RepostNote key={event.id} event={event} className="mt-3" />;
default:
return <TextNote key={event.id} event={event} className="mt-3" />;
}
};
useEffect(() => {
if (!ref.current) return;
const handle = ref.current;
if (offset) {
handle.scrollTo(offset);
}
return () => {
sessionStorage.setItem(
cacheKey,
JSON.stringify([handle.scrollOffset, handle.cache]),
);
};
}, []);
return (
<div className="w-full h-full">
<VList ref={ref} cache={cache} overscan={2} className="flex-1 px-3">
{isLoading ? (
<div className="w-full flex h-16 items-center justify-center gap-2 px-3 py-1.5">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : (
allEvents.map((item) => renderItem(item))
)}
<div className="flex items-center justify-center h-16">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex items-center justify-center w-full h-12 gap-2 font-medium bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800 rounded-xl focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
) : null}
</div>
</VList>
</div>
);
}

View File

@ -1,30 +0,0 @@
import { Column } from "@lume/ark";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { AntenasForm } from "./components/form";
import { HomeRoute } from "./home";
export function Antenas({ column }: { column: LumeColumn }) {
const colKey = `antenas-${column.id}`;
const created = !!column.content?.length;
return (
<Column.Root>
{created ? (
<>
<Column.Header id={column.id} title={column.title} />
<Column.Content>
<Column.Route
path="/"
element={<HomeRoute colKey={colKey} content={column.content} />}
/>
<Column.Route path="/events/:id" element={<EventRoute />} />
<Column.Route path="/users/:id" element={<UserRoute />} />
</Column.Content>
</>
) : (
<AntenasForm id={column.id} />
)}
</Column.Root>
);
}

View File

@ -1,8 +0,0 @@
import sharedConfig from "@lume/tailwindcss";
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
presets: [sharedConfig],
};
export default config;

View File

@ -1,8 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,24 +0,0 @@
{
"name": "@columns/default",
"version": "0.0.0",
"private": true,
"main": "./src/index.tsx",
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.28.4",
"react": "^18.2.0",
"sonner": "^1.4.3",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.66",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2"
}
}

View File

@ -1,188 +0,0 @@
import { Column, useColumnContext } from "@lume/ark";
import { LumeColumn } from "@lume/types";
import { COL_TYPES } from "@lume/utils";
export function Default({ column }: { column: LumeColumn }) {
const { addColumn } = useColumnContext();
return (
<Column.Root>
<Column.Header id={column.id} title="Add columns" />
<div className="h-full flex-1 px-3 mt-3 flex flex-col gap-3 overflow-y-auto scrollbar-none">
<div className="shrink-0 h-11 flex items-center gap-5">
<button
type="button"
className="h-9 w-max px-3 text-sm font-semibold inline-flex items-center justify-center bg-neutral-100 dark:bg-neutral-900 rounded-lg"
>
Official
</button>
<button
type="button"
disabled
className="h-9 w-max px-3 text-sm inline-flex items-center justify-center rounded-lg disabled:opacity-50"
>
Community (Coming Soon)
</button>
</div>
<div className="shrink-0 flex flex-col rounded-xl overflow-hidden bg-neutral-50 dark:bg-neutral-950 ring-1 ring-neutral-100 dark:ring-neutral-900">
<div className="h-[100px] w-full px-3 pt-3">
<img
src="/columns/group.jpg"
srcSet="/columns/group@2x.jpg 2x"
alt="group"
loading="lazy"
decoding="async"
className="w-full h-auto object-cover rounded-lg"
/>
</div>
<div className="h-16 shrink-0 px-3 flex items-center justify-between">
<div>
<h1 className="font-semibold">Group Feeds</h1>
<p className="max-w-[18rem] truncate text-sm text-neutral-600 dark:text-neutral-500">
Collective of people you're interested in.
</p>
</div>
<button
type="button"
onClick={() => {
addColumn({ kind: COL_TYPES.group, title: "", content: "" });
}}
className="shrink-0 w-16 h-8 rounded-lg text-sm font-semibold bg-neutral-100 dark:bg-neutral-900 text-blue-500 hover:bg-neutral-200 dark:hover:bg-neutral-800 inline-flex items-center justify-center"
>
Add
</button>
</div>
</div>
<div className="shrink-0 flex flex-col rounded-xl overflow-hidden bg-neutral-50 dark:bg-neutral-950 ring-1 ring-neutral-100 dark:ring-neutral-900">
<div className="h-[100px] w-full px-3 pt-3">
<img
src="/columns/antenas.jpg"
srcSet="/columns/antenas@2x.jpg 2x"
alt="antenas"
loading="lazy"
decoding="async"
className="w-full h-auto object-cover rounded-lg"
/>
</div>
<div className="h-16 shrink-0 px-3 flex items-center justify-between">
<div>
<h1 className="font-semibold">Antenas</h1>
<p className="max-w-[18rem] truncate text-sm text-neutral-600 dark:text-neutral-500">
Keep track to specific content.
</p>
</div>
<button
type="button"
onClick={() => {
addColumn({ kind: COL_TYPES.antenas, title: "", content: "" });
}}
className="shrink-0 w-16 h-8 rounded-lg text-sm font-semibold bg-neutral-100 dark:bg-neutral-900 text-blue-500 hover:bg-neutral-200 dark:hover:bg-neutral-800 inline-flex items-center justify-center"
>
Add
</button>
</div>
</div>
<div className="shrink-0 flex flex-col rounded-xl overflow-hidden bg-neutral-50 dark:bg-neutral-950 ring-1 ring-neutral-100 dark:ring-neutral-900">
<div className="h-[100px] w-full px-3 pt-3">
<img
src="/columns/trending-notes.jpg"
srcSet="/columns/trending-notes@2x.jpg 2x"
alt="trendingNotes"
loading="lazy"
decoding="async"
className="w-full h-auto object-cover rounded-lg"
/>
</div>
<div className="h-16 shrink-0 px-3 flex items-center justify-between">
<div>
<h1 className="font-semibold">Trending Notes</h1>
<p className="max-w-[18rem] truncate text-sm text-neutral-600 dark:text-neutral-500">
What is trending on Nostr?.
</p>
</div>
<button
type="button"
onClick={() => {
addColumn({
kind: COL_TYPES.trendingNotes,
title: "",
content: "",
});
}}
className="shrink-0 w-16 h-8 rounded-lg text-sm font-semibold bg-neutral-100 dark:bg-neutral-900 text-blue-500 hover:bg-neutral-200 dark:hover:bg-neutral-800 inline-flex items-center justify-center"
>
Add
</button>
</div>
</div>
<div className="shrink-0 flex flex-col rounded-xl overflow-hidden bg-neutral-50 dark:bg-neutral-950 ring-1 ring-neutral-100 dark:ring-neutral-900">
<div className="h-[100px] w-full px-3 pt-3">
<img
src="/columns/global.jpg"
srcSet="/columns/global@2x.jpg 2x"
alt="global"
loading="lazy"
decoding="async"
className="w-full h-auto object-cover rounded-lg"
/>
</div>
<div className="h-16 shrink-0 px-3 flex items-center justify-between">
<div>
<h1 className="font-semibold">Global</h1>
<p className="max-w-[18rem] truncate text-sm text-neutral-600 dark:text-neutral-500">
All things around the world.
</p>
</div>
<button
type="button"
onClick={() => {
addColumn({
kind: COL_TYPES.global,
title: "",
content: "",
});
}}
className="shrink-0 w-16 h-8 rounded-lg text-sm font-semibold bg-neutral-100 dark:bg-neutral-900 text-blue-500 hover:bg-neutral-200 dark:hover:bg-neutral-800 inline-flex items-center justify-center"
>
Add
</button>
</div>
</div>
<div className="shrink-0 flex flex-col rounded-xl overflow-hidden bg-neutral-50 dark:bg-neutral-950 ring-1 ring-neutral-100 dark:ring-neutral-900">
<div className="h-[100px] w-full px-3 pt-3">
<img
src="/columns/waifu.jpg"
srcSet="/columns/waifu@2x.jpg 2x"
alt="waifu"
loading="lazy"
decoding="async"
className="w-full h-auto object-cover rounded-lg"
/>
</div>
<div className="h-16 shrink-0 px-3 flex items-center justify-between">
<div>
<h1 className="font-semibold">Waifu</h1>
<p className="max-w-[18rem] truncate text-sm text-neutral-600 dark:text-neutral-500">
Show a random waifu image to boost your morale.
</p>
</div>
<button
type="button"
onClick={() => {
addColumn({
kind: COL_TYPES.waifu,
title: "Waifu",
content: "",
});
}}
className="shrink-0 w-16 h-8 rounded-lg text-sm font-semibold bg-neutral-100 dark:bg-neutral-900 text-blue-500 hover:bg-neutral-200 dark:hover:bg-neutral-800 inline-flex items-center justify-center"
>
Add
</button>
</div>
</div>
<div className="h-3" />
</div>
</Column.Root>
);
}

View File

@ -1,8 +0,0 @@
import sharedConfig from "@lume/tailwindcss";
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
presets: [sharedConfig],
};
export default config;

View File

@ -1,8 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,24 +0,0 @@
{
"name": "@columns/foryou",
"version": "0.0.0",
"private": true,
"main": "./src/index.tsx",
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.28.4",
"react": "^18.2.0",
"sonner": "^1.4.3",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.66",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2"
}
}

View File

@ -1,139 +0,0 @@
import { TextNote, useArk } from "@lume/ark";
import { InterestModal } from "@lume/ark/src/components/column/interestModal";
import { ArrowRightCircleIcon, ForyouIcon, LoaderIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { EmptyFeed } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react";
import { CacheSnapshot, VList, VListHandle } from "virtua";
export function HomeRoute({ colKey }: { colKey: string }) {
const ark = useArk();
const storage = useStorage();
const ref = useRef<VListHandle>();
const cacheKey = `${colKey}-vlist`;
const queryClient = useQueryClient();
const [offset, cache] = useMemo(() => {
const serialized = sessionStorage.getItem(cacheKey);
if (!serialized) return [];
return JSON.parse(serialized) as [number, CacheSnapshot];
}, []);
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: [colKey],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
if (!storage.interests?.hashtags) return [];
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text],
"#t": storage.interests.hashtags.map((item: string) =>
item.replace("#", "").toLowerCase(),
),
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
initialData: () => {
const queryCacheData = queryClient.getQueryState([colKey])
?.data as NDKEvent[];
if (queryCacheData) {
return {
pageParams: [undefined, 1],
pages: [queryCacheData],
};
}
},
select: (data) => data?.pages.flatMap((page) => page),
staleTime: 120 * 1000,
refetchOnWindowFocus: false,
refetchOnMount: false,
});
useEffect(() => {
if (!ref.current) return;
const handle = ref.current;
if (offset) {
handle.scrollTo(offset);
}
return () => {
sessionStorage.setItem(
cacheKey,
JSON.stringify([handle.scrollOffset, handle.cache]),
);
};
}, []);
if (!storage.interests?.hashtags?.length) {
return (
<div className="px-3 mt-3">
<EmptyFeed subtext="You can more interests to build up your timeline" />
<InterestModal
queryKey={[colKey]}
className="mt-3 w-full text-sm font-medium inline-flex items-center justify-center rounded-lg h-9 bg-blue-500 hover:bg-blue-600 text-white"
>
<ForyouIcon className="size-5" />
Add interest
</InterestModal>
</div>
);
}
return (
<div className="w-full h-full">
<VList ref={ref} cache={cache} overscan={2} className="flex-1 px-3">
{isLoading ? (
<div className="w-full flex h-16 items-center justify-center gap-2 px-3 py-1.5">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : (
data.map((event) => (
<TextNote key={event.id} event={event} className="mt-3" />
))
)}
<div className="flex items-center justify-center h-16">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex items-center justify-center w-full h-12 gap-2 font-medium bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800 rounded-xl focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
) : null}
</div>
</VList>
</div>
);
}

View File

@ -1,49 +0,0 @@
import { Column } from "@lume/ark";
import { useStorage } from "@lume/storage";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useQueryClient } from "@tanstack/react-query";
import { useRef } from "react";
import { HomeRoute } from "./home";
export function ForYou({ column }: { column: LumeColumn }) {
const colKey = `foryou-${column.id}`;
const storage = useStorage();
const queryClient = useQueryClient();
const since = useRef(Math.floor(Date.now() / 1000));
const refresh = async (events: NDKEvent[]) => {
const uniqEvents = new Set(events);
await queryClient.setQueryData(
[colKey],
(prev: { pageParams: number; pages: Array<NDKEvent[]> }) => ({
...prev,
pages: [[...uniqEvents], ...prev.pages],
}),
);
};
return (
<Column.Root>
<Column.Header id={column.id} queryKey={[colKey]} title="For You" />
{storage.interests?.hashtags ? (
<Column.Live
filter={{
kinds: [NDKKind.Text],
"#t": storage.interests.hashtags.map((item: string) =>
item.replace("#", "").toLowerCase(),
),
since: since.current,
}}
onClick={refresh}
/>
) : null}
<Column.Content>
<Column.Route path="/" element={<HomeRoute colKey={colKey} />} />
<Column.Route path="/events/:id" element={<EventRoute />} />
<Column.Route path="/users/:id" element={<UserRoute />} />
</Column.Content>
</Column.Root>
);
}

View File

@ -1,8 +0,0 @@
import sharedConfig from "@lume/tailwindcss";
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
presets: [sharedConfig],
};
export default config;

View File

@ -1,8 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,24 +0,0 @@
{
"name": "@columns/global",
"version": "0.0.0",
"private": true,
"main": "./src/index.tsx",
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.28.4",
"react": "^18.2.0",
"sonner": "^1.4.3",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.66",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2"
}
}

View File

@ -1,126 +0,0 @@
import { RepostNote, TextNote, useArk } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } from "@lume/icons";
import { EmptyFeed } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { type NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react";
import { Link } from "react-router-dom";
import { CacheSnapshot, VList, VListHandle } from "virtua";
export function HomeRoute({ colKey }: { colKey: string }) {
const ark = useArk();
const ref = useRef<VListHandle>();
const cacheKey = `${colKey}-vlist`;
const [offset, cache] = useMemo(() => {
const serialized = sessionStorage.getItem(cacheKey);
if (!serialized) return [];
return JSON.parse(serialized) as [number, CacheSnapshot];
}, []);
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: [colKey],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
if (!ark.account.contacts.length) return [];
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
refetchOnMount: false,
});
const renderItem = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote key={event.id} event={event} className="mt-3" />;
case NDKKind.Repost:
return <RepostNote key={event.id} event={event} className="mt-3" />;
default:
return <TextNote key={event.id} event={event} className="mt-3" />;
}
};
useEffect(() => {
if (!ref.current) return;
const handle = ref.current;
if (offset) {
handle.scrollTo(offset);
}
return () => {
sessionStorage.setItem(
cacheKey,
JSON.stringify([handle.scrollOffset, handle.cache]),
);
};
}, []);
return (
<div className="w-full h-full">
<VList ref={ref} cache={cache} overscan={2} className="flex-1 px-3">
{isLoading ? (
<div className="w-full flex h-16 items-center justify-center gap-2 px-3 py-1.5">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : !data.length ? (
<div className="px-3 mt-3">
<EmptyFeed />
<Link
to="/suggest"
className="mt-3 w-full gap-2 inline-flex items-center justify-center text-sm font-medium rounded-lg h-9 bg-blue-500 hover:bg-blue-600 text-white"
>
<SearchIcon className="size-5" />
Find accounts to follow
</Link>
</div>
) : (
data.map((item) => renderItem(item))
)}
<div className="flex items-center justify-center h-16">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex items-center justify-center w-full h-12 gap-2 font-medium bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800 rounded-xl focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
) : null}
</div>
</VList>
</div>
);
}

View File

@ -1,19 +0,0 @@
import { Column } from "@lume/ark";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function Global({ column }: { column: LumeColumn }) {
const colKey = `global-${column.id}`;
return (
<Column.Root>
<Column.Header id={column.id} queryKey={[colKey]} title="Global" />
<Column.Content>
<Column.Route path="/" element={<HomeRoute colKey={colKey} />} />
<Column.Route path="/events/:id" element={<EventRoute />} />
<Column.Route path="/users/:id" element={<UserRoute />} />
</Column.Content>
</Column.Root>
);
}

View File

@ -1,8 +0,0 @@
import sharedConfig from "@lume/tailwindcss";
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
presets: [sharedConfig],
};
export default config;

View File

@ -1,8 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,24 +0,0 @@
{
"name": "@columns/group",
"version": "0.0.0",
"private": true,
"main": "./src/index.tsx",
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.28.4",
"react": "^18.2.0",
"sonner": "^1.4.3",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.66",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2"
}
}

View File

@ -1,92 +0,0 @@
import { useArk, useColumnContext } from "@lume/ark";
import { CancelIcon, CheckCircleIcon } from "@lume/icons";
import { User } from "@lume/ui";
import { useState } from "react";
export function GroupForm({ id }: { id: number }) {
const ark = useArk();
const { updateColumn, removeColumn } = useColumnContext();
const [title, setTitle] = useState<string>("Just a new group");
const [users, setUsers] = useState<Array<string>>([]);
// toggle follow state
const toggleUser = (pubkey: string) => {
const arr = users.includes(pubkey)
? users.filter((i) => i !== pubkey)
: [...users, pubkey];
setUsers(arr);
};
const submit = async () => {
await updateColumn(id, title, JSON.stringify(users));
};
return (
<div className="flex flex-col justify-between h-full">
<div className="flex flex-col flex-1 min-h-0">
<div className="flex items-center justify-between w-full px-3 border-b h-11 shrink-0 border-neutral-100 dark:border-neutral-900">
<h1 className="text-sm font-semibold">Create a new Group</h1>
<button
type="button"
onClick={async () => await removeColumn(id)}
className="inline-flex items-center justify-center rounded size-6 hover:bg-neutral-100 dark:hover:bg-neutral-900"
>
<CancelIcon className="size-4" />
</button>
</div>
<div className="flex flex-col gap-5 px-3 pt-2 overflow-y-auto">
<div className="flex flex-col gap-1.5">
<label
htmlFor="name"
className="font-medium text-neutral-700 dark:text-neutral-300"
>
Group Name
</label>
<input
name="name"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Nostrichs..."
className="px-2 border border-neutral-100 dark:border-neutral-900 bg-neutral-50 rounded-lg h-10 dark:bg-neutral-950 placeholder:text-neutral-600 focus:border-blue-500 focus:shadow-none focus:ring-0"
/>
</div>
<div className="flex flex-col gap-1">
<div className="inline-flex items-center justify-between">
<span className="font-medium text-neutral-700 dark:text-neutral-300">
Pick user
</span>
<span className="text-sm font-medium text-neutral-600 dark:text-neutral-400">{`${users.length} / ∞`}</span>
</div>
<div className="flex flex-col gap-2">
{ark.account?.contacts?.map((item: string) => (
<button
key={item}
type="button"
onClick={() => toggleUser(item)}
className="inline-flex items-center justify-between px-3 py-2 rounded-xl bg-neutral-50 dark:bg-neutral-950 hover:bg-neutral-100 dark:hover:bg-neutral-900"
>
<User pubkey={item} variant="simple" />
{users.includes(item) ? (
<CheckCircleIcon className="w-5 h-5 text-teal-500" />
) : null}
</button>
))}
<div className="h-20" />
</div>
</div>
</div>
</div>
<div className="absolute z-10 flex items-center justify-center w-full bottom-3">
<button
type="button"
onClick={submit}
disabled={users.length < 1}
className="inline-flex items-center justify-center gap-2 px-6 font-medium text-white transform bg-blue-500 rounded-full active:translate-y-1 w-36 h-11 hover:bg-blue-600 focus:outline-none disabled:cursor-not-allowed"
>
Create
</button>
</div>
</div>
);
}

View File

@ -1,119 +0,0 @@
import { RepostNote, TextNote, useArk } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react";
import { CacheSnapshot, VList, VListHandle } from "virtua";
export function HomeRoute({
colKey,
content,
}: { colKey: string; content: string }) {
const ark = useArk();
const ref = useRef<VListHandle>();
const cacheKey = `${colKey}-vlist`;
const [offset, cache] = useMemo(() => {
const serialized = sessionStorage.getItem(cacheKey);
if (!serialized) return [];
return JSON.parse(serialized) as [number, CacheSnapshot];
}, []);
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: [colKey],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const authors: string[] = JSON.parse(content);
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: authors,
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
refetchOnWindowFocus: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data],
);
const renderItem = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote key={event.id} event={event} className="mt-3" />;
case NDKKind.Repost:
return <RepostNote key={event.id} event={event} className="mt-3" />;
default:
return <TextNote key={event.id} event={event} className="mt-3" />;
}
};
useEffect(() => {
if (!ref.current) return;
const handle = ref.current;
if (offset) {
handle.scrollTo(offset);
}
return () => {
sessionStorage.setItem(
cacheKey,
JSON.stringify([handle.scrollOffset, handle.cache]),
);
};
}, []);
return (
<div className="w-full h-full">
<VList ref={ref} cache={cache} overscan={2} className="flex-1 px-3">
{isLoading ? (
<div className="w-full flex h-16 items-center justify-center gap-2 px-3 py-1.5">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : (
allEvents.map((item) => renderItem(item))
)}
<div className="flex items-center justify-center h-16">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex items-center justify-center w-full h-12 gap-2 font-medium bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800 rounded-xl focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
) : null}
</div>
</VList>
</div>
);
}

View File

@ -1,30 +0,0 @@
import { Column } from "@lume/ark";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { GroupForm } from "./components/form";
import { HomeRoute } from "./home";
export function Group({ column }: { column: LumeColumn }) {
const colKey = `group-${column.id}`;
const created = !!column.content?.length;
return (
<Column.Root>
{created ? (
<>
<Column.Header id={column.id} title={column.title} />
<Column.Content>
<Column.Route
path="/"
element={<HomeRoute colKey={colKey} content={column.content} />}
/>
<Column.Route path="/events/:id" element={<EventRoute />} />
<Column.Route path="/users/:id" element={<UserRoute />} />
</Column.Content>
</>
) : (
<GroupForm id={column.id} />
)}
</Column.Root>
);
}

View File

@ -1,8 +0,0 @@
import sharedConfig from "@lume/tailwindcss";
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
presets: [sharedConfig],
};
export default config;

View File

@ -1,8 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,24 +0,0 @@
{
"name": "@columns/hashtag",
"version": "0.0.0",
"private": true,
"main": "./src/index.tsx",
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.28.4",
"react": "^18.2.0",
"sonner": "^1.4.3",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.66",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2"
}
}

View File

@ -1,109 +0,0 @@
import { TextNote, useArk } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react";
import { CacheSnapshot, VList, VListHandle } from "virtua";
export function HomeRoute({
colKey,
hashtag,
}: { colKey: string; hashtag: string }) {
const ark = useArk();
const ref = useRef<VListHandle>();
const cacheKey = `${colKey}-vlist`;
const [offset, cache] = useMemo(() => {
const serialized = sessionStorage.getItem(cacheKey);
if (!serialized) return [];
return JSON.parse(serialized) as [number, CacheSnapshot];
}, []);
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: [colKey],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text],
"#t": [hashtag],
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
refetchOnWindowFocus: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data],
);
useEffect(() => {
if (!ref.current) return;
const handle = ref.current;
if (offset) {
handle.scrollTo(offset);
}
return () => {
sessionStorage.setItem(
cacheKey,
JSON.stringify([handle.scrollOffset, handle.cache]),
);
};
}, []);
return (
<div className="w-full h-full">
<VList ref={ref} cache={cache} overscan={2} className="flex-1 px-3">
{isLoading ? (
<div className="w-full flex h-16 items-center justify-center gap-2 px-3 py-1.5">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : (
allEvents.map((item) => (
<TextNote key={item.id} event={item} className="mt-3" />
))
)}
<div className="flex items-center justify-center h-16">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex items-center justify-center w-full h-12 gap-2 font-medium bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800 rounded-xl focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
) : null}
</div>
</VList>
</div>
);
}

View File

@ -1,23 +0,0 @@
import { Column } from "@lume/ark";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function Hashtag({ column }: { column: LumeColumn }) {
const colKey = `hashtag-${column.id}`;
const hashtag = column.content.replace("#", "");
return (
<Column.Root>
<Column.Header id={column.id} queryKey={[colKey]} title={hashtag} />
<Column.Content>
<Column.Route
path="/"
element={<HomeRoute colKey={colKey} hashtag={hashtag} />}
/>
<Column.Route path="/events/:id" element={<EventRoute />} />
<Column.Route path="/users/:id" element={<UserRoute />} />
</Column.Content>
</Column.Root>
);
}

View File

@ -1,8 +0,0 @@
import sharedConfig from "@lume/tailwindcss";
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
presets: [sharedConfig],
};
export default config;

View File

@ -1,8 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,24 +0,0 @@
{
"name": "@columns/thread",
"version": "0.0.0",
"private": true,
"main": "./src/index.tsx",
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.28.4",
"react": "^18.2.0",
"sonner": "^1.4.3",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.66",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2"
}
}

View File

@ -1,16 +0,0 @@
import { ThreadNote } from "@lume/ark";
import { ReplyList } from "@lume/ui";
import { WindowVirtualizer } from "virtua";
export function HomeRoute({ id }: { id: string }) {
return (
<div className="overflow-y-auto pb-5">
<WindowVirtualizer>
<div className="mt-3 px-3">
<ThreadNote eventId={id} />
<ReplyList eventId={id} className="mt-5" />
</div>
</WindowVirtualizer>
</div>
);
}

View File

@ -1,17 +0,0 @@
import { Column } from "@lume/ark";
import { LumeColumn } from "@lume/types";
import { HomeRoute } from "./home";
import { EventRoute, UserRoute } from "@lume/ui";
export function Thread({ column }: { column: LumeColumn }) {
return (
<Column.Root>
<Column.Header id={column.id} title={column.title} />
<Column.Content>
<Column.Route path="/" element={<HomeRoute id={column.content} />} />
<Column.Route path="/events/:id" element={<EventRoute />} />
<Column.Route path="/users/:id" element={<UserRoute />} />
</Column.Content>
</Column.Root>
);
}

View File

@ -1,8 +0,0 @@
import sharedConfig from "@lume/tailwindcss";
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
presets: [sharedConfig],
};
export default config;

View File

@ -1,8 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,24 +0,0 @@
{
"name": "@columns/timeline",
"version": "0.0.0",
"private": true,
"main": "./src/index.tsx",
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.28.4",
"react": "^18.2.0",
"sonner": "^1.4.3",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.66",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2"
}
}

View File

@ -1,114 +0,0 @@
import { RepostNote, TextNote, useArk } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } from "@lume/icons";
import { Event, Kind } from "@lume/types";
import { EmptyFeed } from "@lume/ui";
import { FETCH_LIMIT } from "@lume/utils";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react";
import { Link } from "react-router-dom";
import { CacheSnapshot, VList, VListHandle } from "virtua";
export function HomeRoute({ queryKey }: { queryKey: string }) {
const ark = useArk();
const ref = useRef<VListHandle>();
const cacheKey = `${queryKey}-vlist`;
const [offset, cache] = useMemo(() => {
const serialized = sessionStorage.getItem(cacheKey);
if (!serialized) return [];
return JSON.parse(serialized) as [number, CacheSnapshot];
}, []);
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: [queryKey],
initialPageParam: 0,
queryFn: async ({
pageParam,
}: {
pageParam: number;
}) => {
const events = await ark.get_text_events(FETCH_LIMIT, pageParam);
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
select: (data) => data?.pages.flatMap((page) => page),
refetchOnWindowFocus: false,
refetchOnMount: false,
});
const renderItem = (event: Event) => {
switch (event.kind) {
case Kind.Text:
return <TextNote key={event.id} event={event} className="mt-3" />;
case Kind.Repost:
return <RepostNote key={event.id} event={event} className="mt-3" />;
default:
return <TextNote key={event.id} event={event} className="mt-3" />;
}
};
useEffect(() => {
if (!ref.current) return;
const handle = ref.current;
if (offset) {
handle.scrollTo(offset);
}
return () => {
sessionStorage.setItem(
cacheKey,
JSON.stringify([handle.scrollOffset, handle.cache]),
);
};
}, []);
return (
<div className="w-full h-full">
<VList ref={ref} cache={cache} overscan={2} className="flex-1 px-3">
{isLoading ? (
<div className="w-full flex h-16 items-center justify-center gap-2 px-3 py-1.5">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : !data.length ? (
<div className="px-3 mt-3">
<EmptyFeed />
<Link
to="/suggest"
className="mt-3 w-full gap-2 inline-flex items-center justify-center text-sm font-medium rounded-lg h-9 bg-blue-500 hover:bg-blue-600 text-white"
>
<SearchIcon className="size-5" />
Find accounts to follow
</Link>
</div>
) : (
data.map((item) => renderItem(item))
)}
<div className="flex items-center justify-center h-16">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex items-center justify-center w-full h-12 gap-2 font-medium bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800 rounded-xl focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
) : null}
</div>
</VList>
</div>
);
}

View File

@ -1,25 +0,0 @@
import { Column } from "@lume/ark";
import { LumeColumn } from "@lume/types";
import { EventRoute, SuggestRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function Timeline({ column }: { column: LumeColumn }) {
const colKey = `timeline-${column.id}`;
return (
<Column.Provider column={column}>
<Column.Root>
<Column.Header queryKey={[colKey]} />
<Column.Content>
<Column.Route path="/" element={<HomeRoute queryKey={colKey} />} />
<Column.Route path="/events/:id" element={<EventRoute />} />
<Column.Route path="/users/:id" element={<UserRoute />} />
<Column.Route
path="/suggest"
element={<SuggestRoute queryKey={colKey} />}
/>
</Column.Content>
</Column.Root>
</Column.Provider>
);
}

View File

@ -1,8 +0,0 @@
import sharedConfig from "@lume/tailwindcss";
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
presets: [sharedConfig],
};
export default config;

View File

@ -1,8 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,24 +0,0 @@
{
"name": "@columns/trending-notes",
"version": "0.0.0",
"private": true,
"main": "./src/index.tsx",
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.28.4",
"react": "^18.2.0",
"sonner": "^1.4.3",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.66",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2"
}
}

View File

@ -1,71 +0,0 @@
import { TextNote, useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { type NDKEvent, type NostrEvent } from "@nostr-dev-kit/ndk";
import { useQuery } from "@tanstack/react-query";
import { fetch } from "@tauri-apps/plugin-http";
import { useEffect, useMemo, useRef } from "react";
import { CacheSnapshot, VList, VListHandle } from "virtua";
export function HomeRoute({ colKey }: { colKey: string }) {
const ark = useArk();
const ref = useRef<VListHandle>();
const cacheKey = `${colKey}-vlist`;
const [offset, cache] = useMemo(() => {
const serialized = sessionStorage.getItem(cacheKey);
if (!serialized) return [];
return JSON.parse(serialized) as [number, CacheSnapshot];
}, []);
const { data, isLoading } = useQuery({
queryKey: [colKey],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const res = await fetch("https://api.nostr.band/v0/trending/notes", {
signal,
});
if (!res) throw new Error("Failed to fetch trending notes");
const data = await res.json();
const events = data.notes.map((item: { event: NostrEvent }) =>
ark.getNDKEvent(item.event),
);
return events as NDKEvent[];
},
refetchOnMount: false,
refetchOnWindowFocus: false,
});
useEffect(() => {
if (!ref.current) return;
const handle = ref.current;
if (offset) {
handle.scrollTo(offset);
}
return () => {
sessionStorage.setItem(
cacheKey,
JSON.stringify([handle.scrollOffset, handle.cache]),
);
};
}, []);
return (
<div className="w-full h-full">
<VList ref={ref} cache={cache} overscan={2} className="flex-1 px-3">
{isLoading ? (
<div className="w-full flex h-16 items-center justify-center gap-2 px-3 py-1.5">
<LoaderIcon className="size-5 animate-spin" />
</div>
) : (
data.map((item) => (
<TextNote key={item.id} event={item} className="mt-3" />
))
)}
</VList>
</div>
);
}

View File

@ -1,23 +0,0 @@
import { Column } from "@lume/ark";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function TrendingNotes({ column }: { column: LumeColumn }) {
const colKey = `trending-notes-${column.id}`;
return (
<Column.Root>
<Column.Header
id={column.id}
queryKey={[colKey]}
title="Trending Notes"
/>
<Column.Content>
<Column.Route path="/" element={<HomeRoute colKey={colKey} />} />
<Column.Route path="/events/:id" element={<EventRoute />} />
<Column.Route path="/users/:id" element={<UserRoute />} />
</Column.Content>
</Column.Root>
);
}

View File

@ -1,8 +0,0 @@
import sharedConfig from "@lume/tailwindcss";
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
presets: [sharedConfig],
};
export default config;

View File

@ -1,8 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,24 +0,0 @@
{
"name": "@columns/user",
"version": "0.0.0",
"private": true,
"main": "./src/index.tsx",
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.28.4",
"react": "^18.2.0",
"sonner": "^1.4.3",
"virtua": "^0.27.5"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.66",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2"
}
}

View File

@ -1,120 +0,0 @@
import { RepostNote, TextNote, User, useArk } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { WindowVirtualizer } from "virtua";
export function HomeRoute({ id }: { id: string }) {
const ark = useArk();
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["user-posts", id],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: [id],
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
refetchOnWindowFocus: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data],
);
const renderItem = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote key={event.id} event={event} className="mt-3" />;
case NDKKind.Repost:
return <RepostNote key={event.id} event={event} className="mt-3" />;
default:
return <TextNote key={event.id} event={event} className="mt-3" />;
}
};
return (
<div className="py-5 overflow-y-auto">
<WindowVirtualizer>
<div className="px-3">
<User.Provider pubkey={id}>
<User.Root className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<User.Avatar className="h-12 w-12 shrink-0 rounded-lg object-cover" />
<User.Button
target={id}
className="inline-flex items-center justify-center w-24 text-sm font-semibold border-t rounded-lg border-neutral-900 dark:border-neutral-800 h-9 bg-neutral-950 text-neutral-50 dark:bg-neutral-900 hover:bg-neutral-900 dark:hover:bg-neutral-800"
/>
</div>
<div className="flex flex-1 flex-col gap-1.5">
<div className="flex flex-col">
<User.Name className="text-lg font-semibold" />
<User.NIP05
pubkey={id}
className="max-w-[15rem] truncate text-sm text-neutral-600 dark:text-neutral-400"
/>
</div>
<User.About className="text-neutral-900 dark:text-neutral-100" />
</div>
</User.Root>
</User.Provider>
<div className="pt-2 mt-2 border-t border-neutral-100 dark:border-neutral-900">
<h3 className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
Latest posts
</h3>
<div className="flex h-full w-full flex-col justify-between gap-1.5 pb-10">
{isLoading ? (
<div className="flex items-center justify-center">
<LoaderIcon className="w-4 h-4 animate-spin" />
</div>
) : (
allEvents.map((item) => renderItem(item))
)}
<div className="flex items-center justify-center h-16 px-3 pb-3">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex items-center justify-center w-full h-12 gap-2 font-medium bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800 rounded-xl focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="size-5" />
Load more
</>
)}
</button>
) : null}
</div>
</div>
</div>
</div>
</WindowVirtualizer>
</div>
);
}

View File

@ -1,17 +0,0 @@
import { Column } from "@lume/ark";
import { LumeColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function User({ column }: { column: LumeColumn }) {
return (
<Column.Root>
<Column.Header id={column.id} title={column.title} />
<Column.Content>
<Column.Route path="/" element={<HomeRoute id={column.content} />} />
<Column.Route path="/events/:id" element={<EventRoute />} />
<Column.Route path="/users/:id" element={<UserRoute />} />
</Column.Content>
</Column.Root>
);
}

View File

@ -1,8 +0,0 @@
import sharedConfig from "@lume/tailwindcss";
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
presets: [sharedConfig],
};
export default config;

View File

@ -1,8 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,22 +0,0 @@
{
"name": "@columns/waifu",
"version": "0.0.0",
"private": true,
"main": "./src/index.tsx",
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/ui": "workspace:^",
"@lume/utils": "workspace:^",
"@tanstack/react-query": "^5.28.4",
"react": "^18.2.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.66",
"tailwindcss": "^3.4.1",
"typescript": "^5.4.2"
}
}

View File

@ -1,69 +0,0 @@
import { LoaderIcon, RefreshIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { useQuery } from "@tanstack/react-query";
export function HomeRoute({ colKey }: { colKey: string }) {
const { data, isLoading, isError, isRefetching, refetch } = useQuery({
queryKey: [colKey],
queryFn: async ({ signal }: { signal: AbortSignal }) => {
const apiUrl = "https://api.waifu.im/search";
const params = {
included_tags: "waifu",
height: ">=2000",
};
const queryParams = new URLSearchParams(params);
const requestUrl = `${apiUrl}?${queryParams}`;
const res = await fetch(requestUrl, { signal });
if (!res.ok) throw new Error("Failed to get image url");
const data = await res.json();
return data.images[0];
},
refetchOnMount: false,
refetchOnWindowFocus: false,
});
return (
<div className="p-3 h-full flex flex-col justify-center items-center">
{isLoading ? (
<LoaderIcon className="size-5 animate-spin" />
) : isError ? (
<p className="text-center text-sm font-medium">
Failed to get image, please try again later.
</p>
) : (
<div className="relative min-h-0 flex-1 grow-0 w-full rounded-xl flex items-stretch">
<img
src={data.url}
alt={data.signature}
loading="lazy"
decoding="async"
className="object-cover w-full rounded-xl ring-1 ring-black/5 dark:ring-white/5"
/>
<div className="absolute bottom-3 right-3 flex items-center gap-2">
<button
type="button"
onClick={() => refetch()}
className="text-sm font-medium px-2 h-7 inline-flex items-center justify-center bg-black/50 hover:bg-black/30 backdrop-blur-2xl rounded-md text-white"
>
<RefreshIcon
className={cn("size-4", isRefetching ? "animate-spin" : "")}
/>
</button>
<a
href={data.source}
target="_blank"
rel="noreferrer"
className="text-sm font-medium px-2 h-7 inline-flex items-center justify-center bg-black/50 hover:bg-black/30 backdrop-blur-2xl rounded-md text-white"
>
Source
</a>
</div>
</div>
)}
</div>
);
}

View File

@ -1,16 +0,0 @@
import { Column } from "@lume/ark";
import { LumeColumn } from "@lume/types";
import { HomeRoute } from "./home";
export function Waifu({ column }: { column: LumeColumn }) {
const colKey = `waifu-${column.id}`;
return (
<Column.Root>
<Column.Header id={column.id} title={column.title} />
<Column.Content>
<Column.Route path="/" element={<HomeRoute colKey={colKey} />} />
</Column.Content>
</Column.Root>
);
}

View File

@ -1,8 +0,0 @@
import sharedConfig from "@lume/tailwindcss";
const config = {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
presets: [sharedConfig],
};
export default config;

View File

@ -1,8 +0,0 @@
{
"extends": "@lume/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}

View File

@ -1,5 +1,3 @@
import { type Webview } from "@tauri-apps/api/webview";
export interface Settings { export interface Settings {
autoupdate: boolean; autoupdate: boolean;
nsecbunker: boolean; nsecbunker: boolean;
@ -88,11 +86,6 @@ export interface LumeColumn {
description?: string; description?: string;
author?: string; author?: string;
logo?: string; logo?: string;
x?: number;
y?: number;
width?: number;
height?: number;
window?: Webview;
} }
export interface Opengraph { export interface Opengraph {

View File

@ -22,7 +22,7 @@
"@tanstack/react-router": "^1.20.0", "@tanstack/react-router": "^1.20.0",
"framer-motion": "^11.0.14", "framer-motion": "^11.0.14",
"get-urls": "^12.1.0", "get-urls": "^12.1.0",
"media-chrome": "^2.2.5", "media-chrome": "^3.0.2",
"minidenticons": "^4.2.1", "minidenticons": "^4.2.1",
"nanoid": "^5.0.6", "nanoid": "^5.0.6",
"qrcode.react": "^3.1.0", "qrcode.react": "^3.1.0",
@ -34,13 +34,13 @@
"react-hotkeys-hook": "^4.5.0", "react-hotkeys-hook": "^4.5.0",
"react-i18next": "^14.1.0", "react-i18next": "^14.1.0",
"react-string-replace": "^1.1.1", "react-string-replace": "^1.1.1",
"slate": "^0.101.5", "slate": "^0.102.0",
"slate-react": "^0.101.6", "slate-react": "^0.102.0",
"sonner": "^1.4.3", "sonner": "^1.4.3",
"string-strip-html": "^13.4.6", "string-strip-html": "^13.4.6",
"uqr": "^0.1.2", "uqr": "^0.1.2",
"use-debounce": "^10.0.0", "use-debounce": "^10.0.0",
"virtua": "^0.27.5" "virtua": "^0.29.0"
}, },
"devDependencies": { "devDependencies": {
"@lume/tailwindcss": "workspace:^", "@lume/tailwindcss": "workspace:^",

View File

@ -7,6 +7,7 @@ import {
import { Webview } from "@tauri-apps/api/webview"; import { Webview } from "@tauri-apps/api/webview";
import { LumeColumn } from "@lume/types"; import { LumeColumn } from "@lume/types";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { type UnlistenFn } from "@tauri-apps/api/event";
export function Column({ export function Column({
column, column,
@ -19,6 +20,7 @@ export function Column({
const childWindow = useRef<Webview>(null); const childWindow = useRef<Webview>(null);
const divRef = useRef<HTMLDivElement>(null); const divRef = useRef<HTMLDivElement>(null);
const initialRect = useRef<DOMRect>(null); const initialRect = useRef<DOMRect>(null);
const unlisten = useRef<UnlistenFn>(null);
const handleResize = useDebouncedCallback(() => { const handleResize = useDebouncedCallback(() => {
const newRect = divRef.current.getBoundingClientRect(); const newRect = divRef.current.getBoundingClientRect();
if (initialRect.current.height !== newRect.height) { if (initialRect.current.height !== newRect.height) {
@ -26,16 +28,12 @@ export function Column({
new LogicalSize(newRect.width, newRect.height), new LogicalSize(newRect.width, newRect.height),
); );
} }
}, 800); }, 500);
const trackResize = useCallback(async () => { const trackResize = useCallback(async () => {
const unlisten = await mainWindow.onResized(() => { unlisten.current = await mainWindow.onResized(() => {
handleResize(); handleResize();
}); });
return () => {
if (unlisten) unlisten();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -52,27 +50,27 @@ export function Column({
if (!divRef.current) return; if (!divRef.current) return;
if (childWindow.current) return; if (childWindow.current) return;
// get element dimension
const rect = divRef.current.getBoundingClientRect(); const rect = divRef.current.getBoundingClientRect();
const name = column.name.toLowerCase().replace(/\W/g, "");
// create new webview // create new webview
initialRect.current = rect; initialRect.current = rect;
childWindow.current = new Webview( childWindow.current = new Webview(mainWindow, name, {
mainWindow, url: column.content,
column.name.toLowerCase().replace(/\W/g, ""), x: rect.x,
{ y: rect.y,
url: column.content, width: rect.width,
x: rect.x, height: rect.height,
y: rect.y, transparent: true,
width: rect.width, userAgent: "Lume/4.0",
height: rect.height, });
transparent: true,
userAgent: "Lume/4.0",
},
);
// track window resize event // track window resize event
trackResize(); trackResize();
return () => {
if (unlisten.current) unlisten.current();
};
}, []); }, []);
return ( return (

View File

@ -15,8 +15,8 @@
"nostr-tools": "^2.3.1", "nostr-tools": "^2.3.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"slate": "^0.101.5", "slate": "^0.102.0",
"slate-react": "^0.101.6" "slate-react": "^0.102.0"
}, },
"devDependencies": { "devDependencies": {
"@lume/tsconfig": "workspace:^", "@lume/tsconfig": "workspace:^",

View File

@ -121,17 +121,17 @@ importers:
specifier: ^14.1.0 specifier: ^14.1.0
version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) version: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0)
slate: slate:
specifier: ^0.101.5 specifier: ^0.102.0
version: 0.101.5 version: 0.102.0
slate-react: slate-react:
specifier: ^0.101.6 specifier: ^0.102.0
version: 0.101.6(react-dom@18.2.0)(react@18.2.0)(slate@0.101.5) version: 0.102.0(react-dom@18.2.0)(react@18.2.0)(slate@0.102.0)
sonner: sonner:
specifier: ^1.4.3 specifier: ^1.4.3
version: 1.4.3(react-dom@18.2.0)(react@18.2.0) version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
virtua: virtua:
specifier: ^0.27.5 specifier: ^0.29.0
version: 0.27.5(react-dom@18.2.0)(react@18.2.0) version: 0.29.0(react-dom@18.2.0)(react@18.2.0)
devDependencies: devDependencies:
'@lume/tailwindcss': '@lume/tailwindcss':
specifier: workspace:^ specifier: workspace:^
@ -182,8 +182,8 @@ importers:
apps/web: apps/web:
dependencies: dependencies:
'@astrojs/check': '@astrojs/check':
specifier: ^0.4.1 specifier: ^0.5.9
version: 0.4.1(typescript@5.4.2) version: 0.5.9(typescript@5.4.2)
'@astrojs/tailwind': '@astrojs/tailwind':
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.1.0(astro@4.5.5)(tailwindcss@3.4.1) version: 5.1.0(astro@4.5.5)(tailwindcss@3.4.1)
@ -255,8 +255,8 @@ importers:
specifier: ^12.1.0 specifier: ^12.1.0
version: 12.1.0 version: 12.1.0
media-chrome: media-chrome:
specifier: ^2.2.5 specifier: ^3.0.2
version: 2.2.5 version: 3.0.2
minidenticons: minidenticons:
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.2.1 version: 4.2.1
@ -288,8 +288,8 @@ importers:
specifier: ^13.4.6 specifier: ^13.4.6
version: 13.4.6 version: 13.4.6
virtua: virtua:
specifier: ^0.27.5 specifier: ^0.29.0
version: 0.27.5(react-dom@18.2.0)(react@18.2.0) version: 0.29.0(react-dom@18.2.0)(react@18.2.0)
devDependencies: devDependencies:
'@lume/tailwindcss': '@lume/tailwindcss':
specifier: workspace:^ specifier: workspace:^
@ -326,506 +326,6 @@ importers:
specifier: ^5.4.2 specifier: ^5.4.2
version: 5.4.2 version: 5.4.2
packages/lume-column-antenas:
dependencies:
'@lume/ark':
specifier: workspace:^
version: link:../ark
'@lume/icons':
specifier: workspace:^
version: link:../icons
'@lume/ui':
specifier: workspace:^
version: link:../ui
'@lume/utils':
specifier: workspace:^
version: link:../utils
'@tanstack/react-query':
specifier: ^5.28.4
version: 5.28.4(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
sonner:
specifier: ^1.4.3
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
virtua:
specifier: ^0.27.5
version: 0.27.5(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@lume/tailwindcss':
specifier: workspace:^
version: link:../tailwindcss
'@lume/tsconfig':
specifier: workspace:^
version: link:../tsconfig
'@lume/types':
specifier: workspace:^
version: link:../types
'@types/react':
specifier: ^18.2.66
version: 18.2.66
tailwindcss:
specifier: ^3.4.1
version: 3.4.1
typescript:
specifier: ^5.4.2
version: 5.4.2
packages/lume-column-default:
dependencies:
'@lume/ark':
specifier: workspace:^
version: link:../ark
'@lume/icons':
specifier: workspace:^
version: link:../icons
'@lume/ui':
specifier: workspace:^
version: link:../ui
'@lume/utils':
specifier: workspace:^
version: link:../utils
'@tanstack/react-query':
specifier: ^5.28.4
version: 5.28.4(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
sonner:
specifier: ^1.4.3
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
virtua:
specifier: ^0.27.5
version: 0.27.5(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@lume/tailwindcss':
specifier: workspace:^
version: link:../tailwindcss
'@lume/tsconfig':
specifier: workspace:^
version: link:../tsconfig
'@lume/types':
specifier: workspace:^
version: link:../types
'@types/react':
specifier: ^18.2.66
version: 18.2.66
tailwindcss:
specifier: ^3.4.1
version: 3.4.1
typescript:
specifier: ^5.4.2
version: 5.4.2
packages/lume-column-foryou:
dependencies:
'@lume/ark':
specifier: workspace:^
version: link:../ark
'@lume/icons':
specifier: workspace:^
version: link:../icons
'@lume/ui':
specifier: workspace:^
version: link:../ui
'@lume/utils':
specifier: workspace:^
version: link:../utils
'@tanstack/react-query':
specifier: ^5.28.4
version: 5.28.4(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
sonner:
specifier: ^1.4.3
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
virtua:
specifier: ^0.27.5
version: 0.27.5(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@lume/tailwindcss':
specifier: workspace:^
version: link:../tailwindcss
'@lume/tsconfig':
specifier: workspace:^
version: link:../tsconfig
'@lume/types':
specifier: workspace:^
version: link:../types
'@types/react':
specifier: ^18.2.66
version: 18.2.66
tailwindcss:
specifier: ^3.4.1
version: 3.4.1
typescript:
specifier: ^5.4.2
version: 5.4.2
packages/lume-column-global:
dependencies:
'@lume/ark':
specifier: workspace:^
version: link:../ark
'@lume/icons':
specifier: workspace:^
version: link:../icons
'@lume/ui':
specifier: workspace:^
version: link:../ui
'@lume/utils':
specifier: workspace:^
version: link:../utils
'@tanstack/react-query':
specifier: ^5.28.4
version: 5.28.4(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
sonner:
specifier: ^1.4.3
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
virtua:
specifier: ^0.27.5
version: 0.27.5(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@lume/tailwindcss':
specifier: workspace:^
version: link:../tailwindcss
'@lume/tsconfig':
specifier: workspace:^
version: link:../tsconfig
'@lume/types':
specifier: workspace:^
version: link:../types
'@types/react':
specifier: ^18.2.66
version: 18.2.66
tailwindcss:
specifier: ^3.4.1
version: 3.4.1
typescript:
specifier: ^5.4.2
version: 5.4.2
packages/lume-column-group:
dependencies:
'@lume/ark':
specifier: workspace:^
version: link:../ark
'@lume/icons':
specifier: workspace:^
version: link:../icons
'@lume/ui':
specifier: workspace:^
version: link:../ui
'@lume/utils':
specifier: workspace:^
version: link:../utils
'@tanstack/react-query':
specifier: ^5.28.4
version: 5.28.4(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
sonner:
specifier: ^1.4.3
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
virtua:
specifier: ^0.27.5
version: 0.27.5(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@lume/tailwindcss':
specifier: workspace:^
version: link:../tailwindcss
'@lume/tsconfig':
specifier: workspace:^
version: link:../tsconfig
'@lume/types':
specifier: workspace:^
version: link:../types
'@types/react':
specifier: ^18.2.66
version: 18.2.66
tailwindcss:
specifier: ^3.4.1
version: 3.4.1
typescript:
specifier: ^5.4.2
version: 5.4.2
packages/lume-column-hashtag:
dependencies:
'@lume/ark':
specifier: workspace:^
version: link:../ark
'@lume/icons':
specifier: workspace:^
version: link:../icons
'@lume/ui':
specifier: workspace:^
version: link:../ui
'@lume/utils':
specifier: workspace:^
version: link:../utils
'@tanstack/react-query':
specifier: ^5.28.4
version: 5.28.4(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
sonner:
specifier: ^1.4.3
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
virtua:
specifier: ^0.27.5
version: 0.27.5(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@lume/tailwindcss':
specifier: workspace:^
version: link:../tailwindcss
'@lume/tsconfig':
specifier: workspace:^
version: link:../tsconfig
'@lume/types':
specifier: workspace:^
version: link:../types
'@types/react':
specifier: ^18.2.66
version: 18.2.66
tailwindcss:
specifier: ^3.4.1
version: 3.4.1
typescript:
specifier: ^5.4.2
version: 5.4.2
packages/lume-column-thread:
dependencies:
'@lume/ark':
specifier: workspace:^
version: link:../ark
'@lume/icons':
specifier: workspace:^
version: link:../icons
'@lume/ui':
specifier: workspace:^
version: link:../ui
'@lume/utils':
specifier: workspace:^
version: link:../utils
'@tanstack/react-query':
specifier: ^5.28.4
version: 5.28.4(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
sonner:
specifier: ^1.4.3
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
virtua:
specifier: ^0.27.5
version: 0.27.5(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@lume/tailwindcss':
specifier: workspace:^
version: link:../tailwindcss
'@lume/tsconfig':
specifier: workspace:^
version: link:../tsconfig
'@lume/types':
specifier: workspace:^
version: link:../types
'@types/react':
specifier: ^18.2.66
version: 18.2.66
tailwindcss:
specifier: ^3.4.1
version: 3.4.1
typescript:
specifier: ^5.4.2
version: 5.4.2
packages/lume-column-timeline:
dependencies:
'@lume/ark':
specifier: workspace:^
version: link:../ark
'@lume/icons':
specifier: workspace:^
version: link:../icons
'@lume/ui':
specifier: workspace:^
version: link:../ui
'@lume/utils':
specifier: workspace:^
version: link:../utils
'@tanstack/react-query':
specifier: ^5.28.4
version: 5.28.4(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
sonner:
specifier: ^1.4.3
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
virtua:
specifier: ^0.27.5
version: 0.27.5(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@lume/tailwindcss':
specifier: workspace:^
version: link:../tailwindcss
'@lume/tsconfig':
specifier: workspace:^
version: link:../tsconfig
'@lume/types':
specifier: workspace:^
version: link:../types
'@types/react':
specifier: ^18.2.66
version: 18.2.66
tailwindcss:
specifier: ^3.4.1
version: 3.4.1
typescript:
specifier: ^5.4.2
version: 5.4.2
packages/lume-column-trending-notes:
dependencies:
'@lume/ark':
specifier: workspace:^
version: link:../ark
'@lume/icons':
specifier: workspace:^
version: link:../icons
'@lume/ui':
specifier: workspace:^
version: link:../ui
'@lume/utils':
specifier: workspace:^
version: link:../utils
'@tanstack/react-query':
specifier: ^5.28.4
version: 5.28.4(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
sonner:
specifier: ^1.4.3
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
virtua:
specifier: ^0.27.5
version: 0.27.5(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@lume/tailwindcss':
specifier: workspace:^
version: link:../tailwindcss
'@lume/tsconfig':
specifier: workspace:^
version: link:../tsconfig
'@lume/types':
specifier: workspace:^
version: link:../types
'@types/react':
specifier: ^18.2.66
version: 18.2.66
tailwindcss:
specifier: ^3.4.1
version: 3.4.1
typescript:
specifier: ^5.4.2
version: 5.4.2
packages/lume-column-user:
dependencies:
'@lume/ark':
specifier: workspace:^
version: link:../ark
'@lume/icons':
specifier: workspace:^
version: link:../icons
'@lume/ui':
specifier: workspace:^
version: link:../ui
'@lume/utils':
specifier: workspace:^
version: link:../utils
'@tanstack/react-query':
specifier: ^5.28.4
version: 5.28.4(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
sonner:
specifier: ^1.4.3
version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
virtua:
specifier: ^0.27.5
version: 0.27.5(react-dom@18.2.0)(react@18.2.0)
devDependencies:
'@lume/tailwindcss':
specifier: workspace:^
version: link:../tailwindcss
'@lume/tsconfig':
specifier: workspace:^
version: link:../tsconfig
'@lume/types':
specifier: workspace:^
version: link:../types
'@types/react':
specifier: ^18.2.66
version: 18.2.66
tailwindcss:
specifier: ^3.4.1
version: 3.4.1
typescript:
specifier: ^5.4.2
version: 5.4.2
packages/lume-column-waifu:
dependencies:
'@lume/ark':
specifier: workspace:^
version: link:../ark
'@lume/icons':
specifier: workspace:^
version: link:../icons
'@lume/ui':
specifier: workspace:^
version: link:../ui
'@lume/utils':
specifier: workspace:^
version: link:../utils
'@tanstack/react-query':
specifier: ^5.28.4
version: 5.28.4(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
devDependencies:
'@lume/tailwindcss':
specifier: workspace:^
version: link:../tailwindcss
'@lume/tsconfig':
specifier: workspace:^
version: link:../tsconfig
'@lume/types':
specifier: workspace:^
version: link:../types
'@types/react':
specifier: ^18.2.66
version: 18.2.66
tailwindcss:
specifier: ^3.4.1
version: 3.4.1
typescript:
specifier: ^5.4.2
version: 5.4.2
packages/tailwindcss: packages/tailwindcss:
devDependencies: devDependencies:
'@evilmartians/harmony': '@evilmartians/harmony':
@ -909,8 +409,8 @@ importers:
specifier: ^12.1.0 specifier: ^12.1.0
version: 12.1.0 version: 12.1.0
media-chrome: media-chrome:
specifier: ^2.2.5 specifier: ^3.0.2
version: 2.2.5 version: 3.0.2
minidenticons: minidenticons:
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.2.1 version: 4.2.1
@ -945,11 +445,11 @@ importers:
specifier: ^1.1.1 specifier: ^1.1.1
version: 1.1.1 version: 1.1.1
slate: slate:
specifier: ^0.101.5 specifier: ^0.102.0
version: 0.101.5 version: 0.102.0
slate-react: slate-react:
specifier: ^0.101.6 specifier: ^0.102.0
version: 0.101.6(react-dom@18.2.0)(react@18.2.0)(slate@0.101.5) version: 0.102.0(react-dom@18.2.0)(react@18.2.0)(slate@0.102.0)
sonner: sonner:
specifier: ^1.4.3 specifier: ^1.4.3
version: 1.4.3(react-dom@18.2.0)(react@18.2.0) version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
@ -963,8 +463,8 @@ importers:
specifier: ^10.0.0 specifier: ^10.0.0
version: 10.0.0(react@18.2.0) version: 10.0.0(react@18.2.0)
virtua: virtua:
specifier: ^0.27.5 specifier: ^0.29.0
version: 0.27.5(react-dom@18.2.0)(react@18.2.0) version: 0.29.0(react-dom@18.2.0)(react@18.2.0)
devDependencies: devDependencies:
'@lume/tailwindcss': '@lume/tailwindcss':
specifier: workspace:^ specifier: workspace:^
@ -1009,11 +509,11 @@ importers:
specifier: ^18.2.0 specifier: ^18.2.0
version: 18.2.0(react@18.2.0) version: 18.2.0(react@18.2.0)
slate: slate:
specifier: ^0.101.5 specifier: ^0.102.0
version: 0.101.5 version: 0.102.0
slate-react: slate-react:
specifier: ^0.101.6 specifier: ^0.102.0
version: 0.101.6(react-dom@18.2.0)(react@18.2.0)(slate@0.101.5) version: 0.102.0(react-dom@18.2.0)(react@18.2.0)(slate@0.102.0)
devDependencies: devDependencies:
'@lume/tsconfig': '@lume/tsconfig':
specifier: workspace:^ specifier: workspace:^
@ -1048,8 +548,8 @@ packages:
'@jridgewell/trace-mapping': 0.3.25 '@jridgewell/trace-mapping': 0.3.25
dev: false dev: false
/@astrojs/check@0.4.1(typescript@5.4.2): /@astrojs/check@0.5.9(typescript@5.4.2):
resolution: {integrity: sha512-XEsuU4TlWkgcsvdeessq5mXLXV1fejtxIioCPv/FfhTzb1bDYe2BtLiSBK+rFTyD9Hl686YOas9AGNMJcpoRsw==} resolution: {integrity: sha512-+QsQMtYq4oso+gmilJC9HLmdi0glZ+04V/VyyTTPry7n21jqjX9SfgDpLGxMk5cwPC/vwZMkn6ORGPnkZS/L5w==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
typescript: ^5.0.0 typescript: ^5.0.0
@ -5058,8 +4558,8 @@ packages:
'@types/mdast': 4.0.3 '@types/mdast': 4.0.3
dev: false dev: false
/media-chrome@2.2.5: /media-chrome@3.0.2:
resolution: {integrity: sha512-59peAYFlL9ZlFVkKJmIgIDNMkQr4nauYTwIQhLg3khmGfO6/25VNEI8Yn0aUMLb5IFB2gzjcPmfu1ktfOhQ8Ag==} resolution: {integrity: sha512-PdTKNmQ3JDTfd6MQl53qKANx5iSgu3PMNVWDHd2yzjlnvdJwUUuVr1z0vdutig2vYP0DGhC/QRzotfb6m0FfCw==}
dev: false dev: false
/merge-stream@2.0.0: /merge-stream@2.0.0:
@ -6331,8 +5831,8 @@ packages:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
dev: false dev: false
/slate-react@0.101.6(react-dom@18.2.0)(react@18.2.0)(slate@0.101.5): /slate-react@0.102.0(react-dom@18.2.0)(react@18.2.0)(slate@0.102.0):
resolution: {integrity: sha512-aMtp9FY127hKWTkCcTBonfKIwKJC2ESPqFdw2o/RuOk3RMQRwsWay8XTOHx8OBGOHanI2fsKaTAPF5zxOLA1Qg==} resolution: {integrity: sha512-SAcFsK5qaOxXjm0hr/t2pvIxfRv6HJGzmWkG58TdH4LdJCsgKS1n6hQOakHPlRVCwPgwvngB6R+t3pPjv8MqwA==}
peerDependencies: peerDependencies:
react: '>=18.2.0' react: '>=18.2.0'
react-dom: '>=18.2.0' react-dom: '>=18.2.0'
@ -6348,12 +5848,12 @@ packages:
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
scroll-into-view-if-needed: 3.1.0 scroll-into-view-if-needed: 3.1.0
slate: 0.101.5 slate: 0.102.0
tiny-invariant: 1.3.1 tiny-invariant: 1.3.1
dev: false dev: false
/slate@0.101.5: /slate@0.102.0:
resolution: {integrity: sha512-ZZt1ia8ayRqxtpILRMi2a4MfdvwdTu64CorxTVq9vNSd0GQ/t3YDkze6wKjdeUtENmBlq5wNIDInZbx38Hfu5Q==} resolution: {integrity: sha512-RT+tHgqOyZVB1oFV9Pv99ajwh4OUCN9p28QWdnDTIzaN/kZxMsHeQN39UNAgtkZTVVVygFqeg7/R2jiptCvfyA==}
dependencies: dependencies:
immer: 10.0.4 immer: 10.0.4
is-plain-object: 5.0.0 is-plain-object: 5.0.0
@ -7065,8 +6565,8 @@ packages:
vfile-message: 4.0.2 vfile-message: 4.0.2
dev: false dev: false
/virtua@0.27.5(react-dom@18.2.0)(react@18.2.0): /virtua@0.29.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-VeiK2eMCHDmNJvP1GO+DB8rX5ACAxrzFRMGIcqoZK+eqnS25C6lSnuZO4XXLK+RmFkPAoHApMZZTf5ngrpcSMw==} resolution: {integrity: sha512-V7gxQDUGgxe32cLyYhZ+hOxtZLuqKV9icqomE8qAN5HILf1TMuisCEZJVbr+k7GI7K+oOkYdKFiBX23cdXSXmg==}
peerDependencies: peerDependencies:
react: '>=16.14.0' react: '>=16.14.0'
react-dom: '>=16.14.0' react-dom: '>=16.14.0'