Merge pull request #154 from luminous-devs/next

v3.0.1
This commit is contained in:
Ren Amamiya 2024-02-01 08:36:21 +07:00 committed by GitHub
commit da722afed3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
74 changed files with 1339 additions and 1970 deletions

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "modules/depot"]
path = modules/depot
url = https://github.com/luminous-devs/depot.git

View File

@ -10,11 +10,14 @@
"@columns/antenas": "workspace:^",
"@columns/default": "workspace:^",
"@columns/foryou": "workspace:^",
"@columns/global": "workspace:^",
"@columns/group": "workspace:^",
"@columns/hashtag": "workspace:^",
"@columns/thread": "workspace:^",
"@columns/timeline": "workspace:^",
"@columns/trending-notes": "workspace:^",
"@columns/user": "workspace:^",
"@columns/waifu": "workspace:^",
"@getalby/sdk": "^3.2.3",
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
@ -35,8 +38,8 @@
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/react-query": "^5.17.19",
"framer-motion": "^10.18.0",
"i18next": "^23.8.0",
"framer-motion": "^11.0.3",
"i18next": "^23.8.1",
"i18next-resources-to-backend": "^1.2.0",
"jotai": "^2.6.3",
"minidenticons": "^4.2.0",
@ -50,14 +53,14 @@
"react-i18next": "^14.0.1",
"react-router-dom": "^6.21.3",
"smol-toml": "^1.1.4",
"sonner": "^1.3.1",
"virtua": "^0.21.1"
"sonner": "^1.4.0",
"virtua": "^0.23.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/node": "^20.11.6",
"@types/node": "^20.11.10",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react-swc": "^3.5.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 KiB

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

View File

@ -36,7 +36,9 @@ export function CreateAccountScreen() {
onClick={() => setMethod("self")}
className={cn(
"flex flex-col items-start px-4 py-3.5 bg-neutral-900 rounded-xl hover:bg-neutral-800",
method === "self" ? "ring-1 ring-teal-500" : "",
method === "self"
? "ring-1 ring-offset-4 ring-offset-black ring-blue-500"
: "",
)}
>
<p className="font-semibold">{t("signup.selfManageMethod")}</p>
@ -49,25 +51,57 @@ export function CreateAccountScreen() {
onClick={() => setMethod("managed")}
className={cn(
"flex flex-col items-start px-4 py-3.5 bg-neutral-900 rounded-xl hover:bg-neutral-800",
method === "managed" ? "ring-1 ring-teal-500" : "",
method === "managed"
? "ring-1 ring-offset-4 ring-offset-black ring-blue-500"
: "",
)}
>
<p className="font-semibold">{t("signup.providerMethod")}</p>
<div className="inline-flex items-center gap-2">
<p className="font-semibold">{t("signup.providerMethod")}</p>
<span className="text-xs font-medium px-2.5 py-0.5 rounded-full bg-gradient-to-tr from-blue-300 via-sky-500 to-teal-200">
Beta
</span>
</div>
<p className="text-sm font-medium text-neutral-500">
{t("signup.providerMethodDescription")}
</p>
</button>
<button
type="button"
onClick={next}
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
t("global.continue")
)}
</button>
<div className="flex flex-col gap-3">
<button
type="button"
onClick={next}
className="inline-flex items-center justify-center w-full h-12 text-lg font-medium text-white bg-blue-500 rounded-xl hover:bg-blue-600"
>
{loading ? (
<LoaderIcon className="size-5 animate-spin" />
) : (
t("global.continue")
)}
</button>
{method === "managed" ? (
<div className="flex flex-col gap-1 text-sm text-neutral-500">
<p className="text-sm font-semibold text-neutral-300">
Attention:
</p>
<p>
You're chosing Managed by Provider, this feature still in
"Beta".
</p>
<p>
Some functions still missing or not work as expected, you
shouldn't create your main account with this method
</p>
<a
href="https://github.com/kind-0/nsecbunkerd/blob/master/OAUTH-LIKE-FLOW.md"
target="_blank"
rel="noreferrer"
className="text-blue-500"
>
Learn more
</a>
</div>
) : null}
</div>
</div>
</div>
</div>

View File

@ -1,11 +1,14 @@
import { Antenas } from "@columns/antenas";
import { Default } from "@columns/default";
import { ForYou } from "@columns/foryou";
import { Global } from "@columns/global";
import { Group } from "@columns/group";
import { Hashtag } from "@columns/hashtag";
import { Thread } from "@columns/thread";
import { Timeline } from "@columns/timeline";
import { TrendingNotes } from "@columns/trending-notes";
import { User } from "@columns/user";
import { Waifu } from "@columns/waifu";
import { useColumnContext } from "@lume/ark";
import {
ArrowLeftIcon,
@ -45,6 +48,12 @@ export function HomeScreen() {
return <Group key={column.id} column={column} />;
case COL_TYPES.antenas:
return <Antenas key={column.id} column={column} />;
case COL_TYPES.global:
return <Global key={column.id} column={column} />;
case COL_TYPES.trendingNotes:
return <TrendingNotes key={column.id} column={column} />;
case COL_TYPES.waifu:
return <Waifu key={column.id} column={column} />;
default:
return <Default key={column.id} column={column} />;
}

View File

@ -51,11 +51,6 @@ export function ProfileSettingScreen() {
let content = {
...data,
username: data.name,
display_name: data.displayName,
bio: data.about,
image: picture,
cover: banner,
picture,
banner,
};
@ -140,7 +135,7 @@ export function ProfileSettingScreen() {
</label>
<input
type={"text"}
{...register("displayName")}
{...register("display_name")}
spellCheck={false}
className="relative h-11 w-full rounded-lg border-transparent bg-neutral-100 px-3 py-1 text-neutral-900 !outline-none backdrop-blur-xl placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:text-neutral-100"
/>

View File

@ -13,7 +13,7 @@
"@astrojs/check": "^0.4.1",
"@astrojs/tailwind": "^5.1.0",
"@fontsource/geist-mono": "^5.0.1",
"astro": "^4.2.4",
"astro": "^4.2.6",
"astro-seo-meta": "^4.1.0",
"astro-seo-schema": "^4.0.0",
"schema-dts": "^1.1.2",

View File

@ -1,13 +0,0 @@
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 87c23e40..bb84872e 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -50,8 +50,6 @@
"deb": {
"depends": []
},
- "externalBin": ["bin/depot"],
- "resources": ["resources/*"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",

View File

@ -35,15 +35,6 @@ RUN pnpm install --frozen-lockfile
# Path for disable updater
#ADD flatpak/0001-disable-tauri-updater.patch .
#RUN patch -p1 -t -i flatpak/0001-disable-tauri-updater.patch
#ADD flatpak/0002-depot-remove.patch .
#RUN patch -p1 -t -i 0002-depot-remove.patch
# compile depot
#ADD flatpak/build-depot.sh build-depot.sh
#RUN mv flatpak/build-depot.sh build-depot.sh
#RUN sh build-depot.sh
#RUN pnpm run build:depot
#ENV VITE_FLATPAK_RESOURCE="/app/lib/lume/resources/config.toml"

View File

@ -1,15 +0,0 @@
#!/usr/bin/env sh
directory_bin="/lume/src-tauri/bin"
target="x86_64-unknown-linux-gnu"
cd modules/depot
check_directory_keep=$(ls $directory_bin | grep -vE '.keep$' | wc -l)
echo $(ls $directory_bin | grep -vE '.keep$')
if [ $check_directory_keep -eq 0 ]; then
cargo build --release
mv target/release/depot "$directory_bin/depot-$target"
fi

View File

@ -1,50 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>nu.lume.Lume</id>
<launchable type="desktop-id">nu.lume.Lume.desktop</launchable>
<name>Lume</name>
<summary>A cross-platform desktop nostr client</summary>
<developer_name>Ren Amamiya</developer_name>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-only</project_license>
<url type="homepage">https://lume.nu</url>
<url type="bugtracker">https://github.com/luminous-devs/lume/issues</url>
<url type="donation">https://nostree.me/npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445</url>
<supports>
<control>pointing</control>
<control>keyboard</control>
<control>touch</control>
</supports>
<description>
<p>
Lume a cross-platform nostr client, supported nsbunker, chats and notifications
</p>
</description>
<custom>
<value key="Purism::form_factor">workstation</value>
<value key="Purism::form_factor">mobile</value>
</custom>
<screenshots>
<screenshot type="default">
<image> https://raw.githubusercontent.com/kogeletey/lume-nostr/feat/package-flatpak/flatpak/screenshoots/login-screen.png </image>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/kogeletey/lume-nostr/feat/package-flatpak/flatpak/screenshoots/collumns.png</image>
</screenshot>
<screenshot>
<image> https://raw.githubusercontent.com/kogeletey/lume-nostr/feat/package-flatpak/flatpak/screenshoots/home-screen.png </image>
</screenshot>
</screenshots>
<releases>
<release version="2.2.3" date="2023-12-07"/>
</releases>
<content_rating type="oars-1.1"/>
<id>
nu.lume.Lume
</id>
<launchable type="desktop-id">
nu.lume.Lume.desktop
</launchable>
<name>
Lume
</name>
<summary>
A cross-platform desktop nostr client
</summary>
<developer_name>
Ren Amamiya
</developer_name>
<metadata_license>
CC0-1.0
</metadata_license>
<project_license>
GPL-3.0-only
</project_license>
<url type="homepage">
https://lume.nu
</url>
<url type="bugtracker">
https://github.com/luminous-devs/lume/issues
</url>
<url type="donation">
https://nostree.me/npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445
</url>
<supports>
<control>
pointing
</control>
<control>
keyboard
</control>
<control>
touch
</control>
</supports>
<description>
<p>
Lume a cross-platform nostr client, supported nsecbunker, chats and notifications
</p>
</description>
<custom>
<value key="Purism::form_factor">
workstation
</value>
<value key="Purism::form_factor">
mobile
</value>
</custom>
<screenshots>
<screenshot type="default">
<image>
https://raw.githubusercontent.com/luminous-devs/lume/flatpak/screenshots/login-screen.png
</image>
</screenshot>
<screenshot>
<image>
https://raw.githubusercontent.com/luminous-devs/lume/flatpak/screenshots/collumns.png
</image>
</screenshot>
<screenshot>
<image>
https://raw.githubusercontent.com/luminous-devs/lume/flatpak/screenshots/home-screen.png
</image>
</screenshot>
</screenshots>
<releases>
<release version="3.0.0" date="2024-15-01" />
</releases>
<content_rating type="oars-1.1" />
</component>

View File

@ -32,9 +32,8 @@ modules:
buildsystem: simple
build-commands:
- install -Dm755 bin/lume /app/bin/lume
# - install -Dm755 bin/depot /app/bin/depot
# - mkdir -p /app/lib/lume/resources
# - cp -r lib/lume/resources /app/lib/lume/resources
- mkdir -p /app/lib/lume/resources
- cp -r lib/lume/resources /app/lib/lume/resources
- mkdir -p /app/share/icons/hicolor/
- cp -r share/icons/hicolor/ /app/share/icons/
- install -Dm644 nu.lume.Lume.appdata.xml /app/share/metainfo/nu.lume.Lume.appdata.xml

View File

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

Before

Width:  |  Height:  |  Size: 606 KiB

After

Width:  |  Height:  |  Size: 606 KiB

@ -1 +0,0 @@
Subproject commit 22f913f26ff365c6408b005b695200279586211f

View File

@ -35,11 +35,11 @@
"react-i18next": "^14.0.1",
"react-router-dom": "^6.21.3",
"react-string-replace": "^1.1.1",
"sonner": "^1.3.1",
"sonner": "^1.4.0",
"string-strip-html": "^13.4.5",
"tippy.js": "^6.3.7",
"use-context-selector": "^1.4.1",
"virtua": "^0.21.1"
"virtua": "^0.23.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",

View File

@ -62,6 +62,10 @@ export class Ark {
return sub;
}
public getNDKEvent(event: NostrEvent) {
return new NDKEvent(this.ndk, event);
}
public async createEvent({
kind,
tags,

View File

@ -101,7 +101,7 @@ export function ColumnHeader({
className="inline-flex items-center gap-3 px-3 text-sm font-medium text-red-500 rounded-lg h-9 hover:bg-red-500 hover:text-red-50 focus:outline-none"
>
<TrashIcon className="size-4" />
{t("global.Delete")}
{t("global.delete")}
</button>
</DropdownMenu.Item>
</DropdownMenu.Content>

View File

@ -7,12 +7,12 @@ function isImage(url: string) {
export function LinkPreview({ url }: { url: string }) {
const domain = new URL(url);
const { status, data } = useOpenGraph(url);
const { isLoading, isError, data } = useOpenGraph(url);
if (status === "pending") {
if (isLoading) {
return (
<div className="flex flex-col w-full mt-1 mb-2.5 rounded-xl overflow-hidden bg-neutral-100 dark:bg-neutral-900 border border-black/5 dark:border-white/5">
<div className="w-full h-48 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
<div className="w-full h-48 shrink-0 animate-pulse bg-neutral-300 dark:bg-neutral-700" />
<div className="flex flex-col gap-2 px-3 py-3">
<div className="w-2/3 h-3 rounded animate-pulse bg-neutral-300 dark:bg-neutral-700" />
<div className="w-3/4 h-3 rounded animate-pulse bg-neutral-300 dark:bg-neutral-700" />
@ -37,6 +37,19 @@ export function LinkPreview({ url }: { url: string }) {
);
}
if (isError) {
return (
<Link
to={url}
target="_blank"
rel="noreferrer"
className="text-blue-500 hover:text-blue-600"
>
{url}
</Link>
);
}
return (
<Link
to={url}
@ -50,7 +63,7 @@ export function LinkPreview({ url }: { url: string }) {
alt={url}
loading="lazy"
decoding="async"
className="object-cover w-full h-48 bg-white rounded-t-lg"
className="object-cover w-full h-48 shrink-0 bg-white rounded-t-lg"
/>
) : null}
<div className="flex flex-col items-start p-3">

View File

@ -1,4 +1,6 @@
import { cn } from "@lume/utils";
import * as HoverCard from "@radix-ui/react-hover-card";
import { Link } from "react-router-dom";
import { User } from "../user";
import { useNoteContext } from "./provider";
@ -11,16 +13,47 @@ export function NoteUser({
return (
<User.Provider pubkey={event.pubkey}>
<User.Root className={cn("flex items-center gap-3", className)}>
<User.Avatar className="size-9 shrink-0 rounded-lg object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
<div className="flex h-6 flex-1 items-start justify-between gap-2">
<User.Name className="font-semibold text-neutral-950 dark:text-neutral-50" />
<User.Time
time={event.created_at}
className="text-neutral-500 dark:text-neutral-400"
/>
</div>
</User.Root>
<HoverCard.Root>
<User.Root className={cn("flex items-center gap-3", className)}>
<HoverCard.Trigger>
<User.Avatar className="size-9 shrink-0 rounded-lg object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
</HoverCard.Trigger>
<div className="flex h-6 flex-1 items-start justify-between gap-2">
<User.Name className="font-semibold text-neutral-950 dark:text-neutral-50" />
<User.Time
time={event.created_at}
className="text-neutral-500 dark:text-neutral-400"
/>
</div>
</User.Root>
<HoverCard.Portal>
<HoverCard.Content
className="data-[side=bottom]:animate-slideUpAndFade w-[300px] shadow-lg shadow-neutral-500/20 rounded-xl bg-white dark:shadow-none dark:bg-neutral-900 dark:border dark:border-neutral-800 p-5 data-[state=open]:transition-all"
sideOffset={5}
>
<div className="flex flex-col gap-2">
<User.Avatar className="size-11 rounded-lg object-cover ring-1 ring-neutral-200/50 dark:ring-neutral-800/50" />
<div className="flex flex-col gap-2">
<div>
<User.Name className="font-semibold leading-tight" />
<User.NIP05
pubkey={event.pubkey}
className="text-neutral-600 dark:text-neutral-400"
/>
</div>
<User.About className="line-clamp-3" />
<Link
to={`/users/${event.pubkey}`}
className="mt-3 w-full h-8 text-sm font-medium bg-neutral-100 dark:bg-neutral-900 hover:bg-neutral-200 dark:hover:bg-neutral-800 rounded-lg inline-flex items-center justify-center"
>
View profile
</Link>
</div>
</div>
<HoverCard.Arrow className="fill-white dark:fill-neutral-800" />
</HoverCard.Content>
</HoverCard.Portal>
</HoverCard.Root>
</User.Provider>
);
}

View File

@ -16,7 +16,7 @@ export function UserName({ className }: { className?: string }) {
}
return (
<div className={cn("truncate", className)}>
<div className={cn("max-w-[12rem] truncate", className)}>
{user.displayName || user.name || "Anon"}
</div>
);

View File

@ -228,7 +228,7 @@ export const LumeProvider = ({ children }: PropsWithChildren<object>) => {
return (
<div
data-tauri-drag-region
className="relative flex items-center justify-center w-screen h-screen"
className="relative flex items-center justify-center w-screen h-screen bg-white dark:bg-black"
>
<div className="flex flex-col items-start max-w-2xl gap-1">
<h5 className="font-semibold uppercase">TIP:</h5>

View File

@ -12,15 +12,15 @@
"@tanstack/react-query": "^5.17.19",
"react": "^18.2.0",
"react-router-dom": "^6.21.3",
"sonner": "^1.3.1",
"virtua": "^0.21.1"
"sonner": "^1.4.0",
"virtua": "^0.23.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.48",
"tailwind": "^4.0.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}
}

View File

@ -1,9 +1,8 @@
import { Column } from "@lume/ark";
import { GroupFeedsIcon } from "@lume/icons";
import { IColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { AntenasForm } from "./components/form";
import { HomeRoute } from "./home";
import { EventRoute, UserRoute } from "@lume/ui";
export function Antenas({ column }: { column: IColumn }) {
const colKey = `antenas-${column.id}`;
@ -13,11 +12,7 @@ export function Antenas({ column }: { column: IColumn }) {
<Column.Root>
{created ? (
<>
<Column.Header
id={column.id}
title={column.title}
icon={<GroupFeedsIcon className="size-4" />}
/>
<Column.Header id={column.id} title={column.title} />
<Column.Content>
<Column.Route
path="/"

View File

@ -12,15 +12,15 @@
"@tanstack/react-query": "^5.17.19",
"react": "^18.2.0",
"react-router-dom": "^6.21.3",
"sonner": "^1.3.1",
"virtua": "^0.21.1"
"sonner": "^1.4.0",
"virtua": "^0.23.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.48",
"tailwind": "^4.0.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}
}

View File

@ -1,5 +1,4 @@
import { Column, useColumnContext } from "@lume/ark";
import { ColumnIcon } from "@lume/icons";
import { IColumn } from "@lume/types";
import { COL_TYPES } from "@lume/utils";
@ -8,13 +7,9 @@ export function Default({ column }: { column: IColumn }) {
return (
<Column.Root>
<Column.Header
id={column.id}
title="Add columns"
icon={<ColumnIcon className="size-4" />}
/>
<div className="h-full px-3 mt-3 flex flex-col gap-3 overflow-y-auto scrollbar-none">
<div className="h-11 flex items-center gap-5">
<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"
@ -29,7 +24,7 @@ export function Default({ column }: { column: IColumn }) {
Community (Coming Soon)
</button>
</div>
<div className="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="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"
@ -58,7 +53,7 @@ export function Default({ column }: { column: IColumn }) {
</button>
</div>
</div>
<div className="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="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"
@ -87,6 +82,106 @@ export function Default({ column }: { column: IColumn }) {
</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

@ -13,15 +13,15 @@
"@tanstack/react-query": "^5.17.19",
"react": "^18.2.0",
"react-router-dom": "^6.21.3",
"sonner": "^1.3.1",
"virtua": "^0.21.1"
"sonner": "^1.4.0",
"virtua": "^0.23.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.48",
"tailwind": "^4.0.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}
}

View File

@ -1,5 +1,4 @@
import { Column } from "@lume/ark";
import { ForyouIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { IColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
@ -27,12 +26,7 @@ export function ForYou({ column }: { column: IColumn }) {
return (
<Column.Root>
<Column.Header
id={column.id}
queryKey={[colKey]}
title="For You"
icon={<ForyouIcon className="size-4" />}
/>
<Column.Header id={column.id} queryKey={[colKey]} title="For You" />
{storage.interests?.hashtags ? (
<Column.Live
filter={{

View File

@ -0,0 +1,26 @@
{
"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:^",
"@nostr-dev-kit/ndk": "^2.3.3",
"@tanstack/react-query": "^5.17.19",
"react": "^18.2.0",
"react-router-dom": "^6.21.3",
"sonner": "^1.4.0",
"virtua": "^0.23.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.48",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}
}

View File

@ -0,0 +1,126 @@
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

@ -0,0 +1,19 @@
import { Column } from "@lume/ark";
import { IColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function Global({ column }: { column: IColumn }) {
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

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

View File

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

View File

@ -12,15 +12,15 @@
"@tanstack/react-query": "^5.17.19",
"react": "^18.2.0",
"react-router-dom": "^6.21.3",
"sonner": "^1.3.1",
"virtua": "^0.21.1"
"sonner": "^1.4.0",
"virtua": "^0.23.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.48",
"tailwind": "^4.0.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}
}

View File

@ -1,9 +1,8 @@
import { Column } from "@lume/ark";
import { GroupFeedsIcon } from "@lume/icons";
import { IColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { GroupForm } from "./components/form";
import { HomeRoute } from "./home";
import { EventRoute, UserRoute } from "@lume/ui";
export function Group({ column }: { column: IColumn }) {
const colKey = `group-${column.id}`;
@ -13,11 +12,7 @@ export function Group({ column }: { column: IColumn }) {
<Column.Root>
{created ? (
<>
<Column.Header
id={column.id}
title={column.title}
icon={<GroupFeedsIcon className="size-4" />}
/>
<Column.Header id={column.id} title={column.title} />
<Column.Content>
<Column.Route
path="/"

View File

@ -12,15 +12,15 @@
"@tanstack/react-query": "^5.17.19",
"react": "^18.2.0",
"react-router-dom": "^6.21.3",
"sonner": "^1.3.1",
"virtua": "^0.21.1"
"sonner": "^1.4.0",
"virtua": "^0.23.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.48",
"tailwind": "^4.0.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}
}

View File

@ -1,8 +1,7 @@
import { Column } from "@lume/ark";
import { HashtagIcon } from "@lume/icons";
import { IColumn } from "@lume/types";
import { HomeRoute } from "./home";
import { EventRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function Hashtag({ column }: { column: IColumn }) {
const colKey = `hashtag-${column.id}`;
@ -10,12 +9,7 @@ export function Hashtag({ column }: { column: IColumn }) {
return (
<Column.Root>
<Column.Header
id={column.id}
queryKey={[colKey]}
title={hashtag}
icon={<HashtagIcon className="size-4" />}
/>
<Column.Header id={column.id} queryKey={[colKey]} title={hashtag} />
<Column.Content>
<Column.Route
path="/"

View File

@ -12,15 +12,15 @@
"@tanstack/react-query": "^5.17.19",
"react": "^18.2.0",
"react-router-dom": "^6.21.3",
"sonner": "^1.3.1",
"virtua": "^0.21.1"
"sonner": "^1.4.0",
"virtua": "^0.23.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.48",
"tailwind": "^4.0.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}
}

View File

@ -12,15 +12,15 @@
"@tanstack/react-query": "^5.17.19",
"react": "^18.2.0",
"react-router-dom": "^6.21.3",
"sonner": "^1.3.1",
"virtua": "^0.21.1"
"sonner": "^1.4.0",
"virtua": "^0.23.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.48",
"tailwind": "^4.0.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}
}

View File

@ -3,7 +3,7 @@ import { ArrowRightCircleIcon, LoaderIcon, SearchIcon } from "@lume/icons";
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 { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react";
import { Link } from "react-router-dom";
import { CacheSnapshot, VList, VListHandle } from "virtua";
@ -12,7 +12,6 @@ export function HomeRoute({ colKey }: { colKey: string }) {
const ark = useArk();
const ref = useRef<VListHandle>();
const cacheKey = `${colKey}-vlist`;
const queryClient = useQueryClient();
const [offset, cache] = useMemo(() => {
const serialized = sessionStorage.getItem(cacheKey);

View File

@ -1,5 +1,4 @@
import { Column, useArk } from "@lume/ark";
import { TimelineIcon } from "@lume/icons";
import { IColumn } from "@lume/types";
import { EventRoute, SuggestRoute, UserRoute } from "@lume/ui";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
@ -26,12 +25,7 @@ export function Timeline({ column }: { column: IColumn }) {
return (
<Column.Root>
<Column.Header
id={column.id}
queryKey={[colKey]}
title="Timeline"
icon={<TimelineIcon className="size-5" />}
/>
<Column.Header id={column.id} queryKey={[colKey]} title="Timeline" />
{ark.account.contacts.length ? (
<Column.Live
filter={{

View File

@ -0,0 +1,26 @@
{
"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:^",
"@nostr-dev-kit/ndk": "^2.3.3",
"@tanstack/react-query": "^5.17.19",
"react": "^18.2.0",
"react-router-dom": "^6.21.3",
"sonner": "^1.4.0",
"virtua": "^0.23.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.48",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}
}

View File

@ -0,0 +1,71 @@
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

@ -0,0 +1,23 @@
import { Column } from "@lume/ark";
import { IColumn } from "@lume/types";
import { EventRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function TrendingNotes({ column }: { column: IColumn }) {
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

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

View File

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

View File

@ -12,15 +12,15 @@
"@tanstack/react-query": "^5.17.19",
"react": "^18.2.0",
"react-router-dom": "^6.21.3",
"sonner": "^1.3.1",
"virtua": "^0.21.1"
"sonner": "^1.4.0",
"virtua": "^0.23.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.48",
"tailwind": "^4.0.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}
}

View File

@ -1,17 +1,12 @@
import { Column } from "@lume/ark";
import { UserIcon } from "@lume/icons";
import { IColumn } from "@lume/types";
import { HomeRoute } from "./home";
import { EventRoute, UserRoute } from "@lume/ui";
import { HomeRoute } from "./home";
export function User({ column }: { column: IColumn }) {
return (
<Column.Root>
<Column.Header
id={column.id}
title={column.title}
icon={<UserIcon className="size-4" />}
/>
<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 />} />

View File

@ -0,0 +1,23 @@
{
"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.17.19",
"react": "^18.2.0",
"react-router-dom": "^6.21.3"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@types/react": "^18.2.48",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3"
}
}

View File

@ -0,0 +1,69 @@
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

@ -0,0 +1,16 @@
import { Column } from "@lume/ark";
import { IColumn } from "@lume/types";
import { HomeRoute } from "./home";
export function Waifu({ column }: { column: IColumn }) {
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

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

View File

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

View File

@ -10,7 +10,7 @@
"dependencies": {
"@lume/storage": "workspace:*",
"@nostr-dev-kit/ndk": "^2.3.3",
"lru-cache": "^10.1.0",
"lru-cache": "^10.2.0",
"nostr-fetch": "^0.15.0",
"nostr-tools": "1.17.0",
"react": "^18.2.0"

View File

@ -18,7 +18,7 @@
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7",
"@tanstack/react-query": "^5.17.19",
"framer-motion": "^10.18.0",
"framer-motion": "^11.0.3",
"jotai": "^2.6.3",
"minidenticons": "^4.2.0",
"nostr-tools": "~1.17.0",
@ -30,10 +30,10 @@
"react-router-dom": "^6.21.3",
"slate": "^0.101.5",
"slate-react": "^0.101.6",
"sonner": "^1.3.1",
"sonner": "^1.4.0",
"uqr": "^0.1.2",
"use-debounce": "^10.0.0",
"virtua": "^0.21.1"
"virtua": "^0.23.0"
},
"devDependencies": {
"@lume/tailwindcss": "workspace:^",

View File

@ -159,7 +159,7 @@ export function SearchDialog() {
</Command.Group>
</>
)}
{!loading ? (
{!loading && !events.length ? (
<div className="h-full flex items-center justify-center flex-col gap-3">
<div className="size-16 bg-blue-100 dark:bg-blue-900 rounded-full inline-flex items-center justify-center text-blue-500">
<SearchIcon className="size-6" />

View File

@ -57,9 +57,9 @@ export const COL_TYPES = {
hashtag: 3,
group: 4,
antenas: 5,
topic: 6,
global: 6,
trendingNotes: 9000,
trendingAccounts: 9001,
waifu: 9001,
foryou: 9998,
newsfeed: 9999,
};

View File

@ -3,10 +3,10 @@ import { useQuery } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
export function useOpenGraph(url: string) {
const { status, data, error } = useQuery({
const { isLoading, isError, data } = useQuery({
queryKey: ["opg", url],
queryFn: async () => {
const res: Opengraph = await invoke("opengraph", { url });
const res: Opengraph = await invoke("fetch_opg", { url });
if (!res) {
throw new Error("fetch preview failed");
}
@ -19,8 +19,8 @@ export function useOpenGraph(url: string) {
});
return {
status,
isLoading,
isError,
data,
error,
};
}

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +0,0 @@
const fs = require('fs')
let extension = ''
if (process.platform === 'win32') {
extension = '.exe'
}
async function main() {
const host = Bun.spawn(["rustc", '-vV']);
const stdoutStr = await new Response(host.stdout).text();
const targetTriple = /host: (\S+)/g.exec(stdoutStr)[1]
if (!targetTriple) {
console.error('Failed to determine platform target triple')
}
fs.renameSync(
`src-tauri/bins/depot${extension}`,
`src-tauri/bins/depot-${targetTriple}${extension}`,
)
}
main().catch((e) => {
throw e
})

91
src-tauri/Cargo.lock generated
View File

@ -114,9 +114,9 @@ dependencies = [
[[package]]
name = "anstyle"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
checksum = "2faccea4cc4ab4a667ce676a30e8ec13922a692c99bb8f5b11f1502c72e04220"
[[package]]
name = "anstyle-parse"
@ -242,9 +242,9 @@ dependencies = [
[[package]]
name = "async-io"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb41eb19024a91746eba0773aa5e16036045bbf45733766661099e182ea6a744"
checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65"
dependencies = [
"async-lock 3.3.0",
"cfg-if",
@ -313,7 +313,7 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5"
dependencies = [
"async-io 2.3.0",
"async-io 2.3.1",
"async-lock 2.8.0",
"atomic-waker",
"cfg-if",
@ -705,9 +705,9 @@ checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "chrono"
version = "0.4.32"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41daef31d7a747c5c847246f36de49ced6f7403b4cdabc807a97b5cc184cda7a"
checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb"
dependencies = [
"android-tzdata",
"iana-time-zone",
@ -1067,9 +1067,9 @@ dependencies = [
[[package]]
name = "darling"
version = "0.20.3"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
checksum = "da01daa5f6d41c91358398e8db4dde38e292378da1f28300b59ef4732b879454"
dependencies = [
"darling_core",
"darling_macro",
@ -1077,9 +1077,9 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.20.3"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
checksum = "f44f6238b948a3c6c3073cdf53bb0c2d5e024ee27e0f35bfe9d556a12395808a"
dependencies = [
"fnv",
"ident_case",
@ -1091,9 +1091,9 @@ dependencies = [
[[package]]
name = "darling_macro"
version = "0.20.3"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
checksum = "0d2d88bd93979b1feb760a6b5c531ac5ba06bd63e74894c377af02faee07b9cd"
dependencies = [
"darling_core",
"quote",
@ -1953,7 +1953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
dependencies = [
"heck",
"proc-macro-crate 2.0.1",
"proc-macro-crate 2.0.2",
"proc-macro-error",
"proc-macro2",
"quote",
@ -2072,7 +2072,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http",
"indexmap 2.1.0",
"indexmap 2.2.1",
"slab",
"tokio",
"tokio-util",
@ -2319,9 +2319,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.1.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
checksum = "433de089bd45971eecf4668ee0ee8f4cec17db4f8bd8f7bc3197a6ce37aa7d9b"
dependencies = [
"equivalent",
"hashbrown 0.14.3",
@ -2393,9 +2393,9 @@ dependencies = [
[[package]]
name = "itertools"
version = "0.12.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
@ -2506,9 +2506,9 @@ dependencies = [
[[package]]
name = "keyring"
version = "2.3.1"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85b479dcf9eae65481044dfda57af7fe2da6c1401180360f6898801fe9ed4db9"
checksum = "1be8bc4c6b6e9d85ecdad090fcf342a9216f53d747a537cc05e3452fd650ca46"
dependencies = [
"byteorder",
"lazy_static",
@ -2610,9 +2610,9 @@ dependencies = [
[[package]]
name = "libz-sys"
version = "1.1.14"
version = "1.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "295c17e837573c8c821dbaeb3cceb3d745ad082f7572191409e69cbc1b3fd050"
checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6"
dependencies = [
"cc",
"libc",
@ -3599,7 +3599,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef"
dependencies = [
"base64",
"indexmap 2.1.0",
"indexmap 2.2.1",
"line-wrap",
"quick-xml 0.31.0",
"serde",
@ -3679,9 +3679,9 @@ dependencies = [
[[package]]
name = "proc-macro-crate"
version = "2.0.1"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97dc5fea232fc28d2f597b37c4876b348a40e33f3b02cc975c8d006d78d94b1a"
checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
dependencies = [
"toml_datetime",
"toml_edit 0.20.2",
@ -3898,7 +3898,7 @@ checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.4",
"regex-automata 0.4.5",
"regex-syntax 0.8.2",
]
@ -3913,9 +3913,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.4"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b7fa1134405e2ec9353fd416b17f8dacd46c473d7d3fd1cf202706a14eb792a"
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
dependencies = [
"aho-corasick",
"memchr",
@ -4204,18 +4204,18 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.195"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.195"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
dependencies = [
"proc-macro2",
"quote",
@ -4224,9 +4224,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.111"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [
"itoa 1.0.10",
"ryu",
@ -4275,7 +4275,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.1.0",
"indexmap 2.2.1",
"serde",
"serde_json",
"serde_with_macros",
@ -4555,7 +4555,7 @@ dependencies = [
"futures-util",
"hashlink",
"hex",
"indexmap 2.1.0",
"indexmap 2.2.1",
"log",
"memchr",
"once_cell",
@ -5630,7 +5630,7 @@ version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.1.0",
"indexmap 2.2.1",
"serde",
"serde_spanned",
"toml_datetime",
@ -5643,7 +5643,7 @@ version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
dependencies = [
"indexmap 2.1.0",
"indexmap 2.2.1",
"serde",
"serde_spanned",
"toml_datetime",
@ -5720,9 +5720,9 @@ dependencies = [
[[package]]
name = "tray-icon"
version = "0.11.1"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fad962d06d2bfd9b2ab4f665fc73b175523b834b1466a294520201c5845145f8"
checksum = "fd26786733426b0bf632ebab410c162859a911f26c7c9e208b9e329a8ca94481"
dependencies = [
"cocoa",
"core-graphics 0.23.1",
@ -6064,15 +6064,16 @@ dependencies = [
[[package]]
name = "webpage"
version = "1.6.0"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8598785beeb5af95abe95e7bb20c7e747d1188347080d6811d5a56d2b9a5f368"
checksum = "3fb86b12e58d490a99867f561ce8466ffa7b73e24d015a8e7f5bc111d4424ba2"
dependencies = [
"curl",
"html5ever",
"markup5ever_rcdom",
"serde",
"serde_json",
"url",
]
[[package]]
@ -6456,9 +6457,9 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "winnow"
version = "0.5.34"
version = "0.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16"
checksum = "1931d78a9c73861da0134f453bb1f790ce49b2e30eba8410b4b79bac72b46a2d"
dependencies = [
"memchr",
]

View File

@ -6,7 +6,7 @@ authors = ["Ren Amamiya"]
license = "GPL-3.0"
repository = "https://github.com/luminous-devs/lume"
edition = "2021"
rust-version = "1.66"
rust-version = "1.70"
[build-dependencies]
tauri-build = { version = "2.0.0-alpha", features = [] }
@ -40,9 +40,14 @@ tauri-plugin-sql = {version="2.0.0-alpha", features = [
sqlx-cli = { version = "0.7.0", default-features = false, features = [
"sqlite",
] }
webpage = { version = "1.6.0", features = ["serde"] }
webpage = { version = "2.0", features = ["serde"] }
[target.'cfg(not(target_os = "linux"))'.dependencies]
keyring = "2"
[target.'cfg(target_os = "linux")'.dependencies]
keyring = { version = "2", default_features = false, features = ["linux-secret-service"] }
[features]
# by default Tauri runs in production mode
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL

View File

@ -1,140 +1,3 @@
use keyring::Entry;
use std::process::Command;
use std::time::Duration;
use webpage::{Webpage, WebpageOptions};
#[tauri::command]
pub async fn show_in_folder(path: String) {
#[cfg(target_os = "windows")]
{
Command::new("explorer")
.args(["/select,", &path]) // The comma after select is not a typo
.spawn()
.unwrap();
}
#[cfg(target_os = "linux")]
{
use std::fs::metadata;
use std::path::PathBuf;
if path.contains(",") {
// see https://gitlab.freedesktop.org/dbus/dbus/-/issues/76
let new_path = match metadata(&path).unwrap().is_dir() {
true => path,
false => {
let mut path2 = PathBuf::from(path);
path2.pop();
path2.into_os_string().into_string().unwrap()
}
};
Command::new("xdg-open").arg(&new_path).spawn().unwrap();
} else {
Command::new("dbus-send")
.args([
"--session",
"--dest=org.freedesktop.FileManager1",
"--type=method_call",
"/org/freedesktop/FileManager1",
"org.freedesktop.FileManager1.ShowItems",
format!("array:string:file://{path}").as_str(),
"string:\"\"",
])
.spawn()
.unwrap();
}
}
#[cfg(target_os = "macos")]
{
Command::new("open").args(["-R", &path]).spawn().unwrap();
}
}
#[derive(serde::Serialize)]
pub struct OpenGraphResponse {
title: String,
description: String,
url: String,
image: String,
}
pub async fn fetch_opengraph(url: String) -> OpenGraphResponse {
let options = WebpageOptions {
allow_insecure: false,
max_redirections: 3,
timeout: Duration::from_secs(15),
useragent: "lume - desktop app".to_string(),
..Default::default()
};
let result = match Webpage::from_url(&url, options) {
Ok(webpage) => webpage,
Err(_) => {
return OpenGraphResponse {
title: "".to_string(),
description: "".to_string(),
url: "".to_string(),
image: "".to_string(),
}
}
};
let html = result.html;
return OpenGraphResponse {
title: html
.opengraph
.properties
.get("title")
.cloned()
.unwrap_or_default(),
description: html
.opengraph
.properties
.get("description")
.cloned()
.unwrap_or_default(),
url: html
.opengraph
.properties
.get("url")
.cloned()
.unwrap_or_default(),
image: html
.opengraph
.images
.get(0)
.and_then(|i| Some(i.url.clone()))
.unwrap_or_default(),
};
}
#[tauri::command]
pub async fn opengraph(url: String) -> OpenGraphResponse {
let result = fetch_opengraph(url).await;
return result;
}
#[tauri::command]
pub fn secure_save(key: String, value: String) -> Result<(), ()> {
let entry = Entry::new("Lume", &key).expect("Failed to create entry");
let _ = entry.set_password(&value);
Ok(())
}
#[tauri::command]
pub fn secure_load(key: String) -> Result<String, String> {
let entry = Entry::new("Lume", &key).expect("Failed to create entry");
if let Ok(password) = entry.get_password() {
Ok(password)
} else {
Err("not found".to_string())
}
}
#[tauri::command]
pub fn secure_remove(key: String) -> Result<(), ()> {
let entry = Entry::new("Lume", &key).expect("Failed to create entry");
let _ = entry.delete_password();
Ok(())
}
pub mod folder;
pub mod opg;
pub mod secret;

View File

@ -0,0 +1,48 @@
use std::process::Command;
#[tauri::command]
pub async fn show_in_folder(path: String) {
#[cfg(target_os = "windows")]
{
Command::new("explorer")
.args(["/select,", &path]) // The comma after select is not a typo
.spawn()
.unwrap();
}
#[cfg(target_os = "linux")]
{
use std::fs::metadata;
use std::path::PathBuf;
if path.contains(",") {
// see https://gitlab.freedesktop.org/dbus/dbus/-/issues/76
let new_path = match metadata(&path).unwrap().is_dir() {
true => path,
false => {
let mut path2 = PathBuf::from(path);
path2.pop();
path2.into_os_string().into_string().unwrap()
}
};
Command::new("xdg-open").arg(&new_path).spawn().unwrap();
} else {
Command::new("dbus-send")
.args([
"--session",
"--dest=org.freedesktop.FileManager1",
"--type=method_call",
"/org/freedesktop/FileManager1",
"org.freedesktop.FileManager1.ShowItems",
format!("array:string:file://{path}").as_str(),
"string:\"\"",
])
.spawn()
.unwrap();
}
}
#[cfg(target_os = "macos")]
{
Command::new("open").args(["-R", &path]).spawn().unwrap();
}
}

View File

@ -0,0 +1,50 @@
use std::time::Duration;
use webpage::{Webpage, WebpageOptions};
#[derive(serde::Serialize)]
pub struct OpenGraphResponse {
title: String,
description: String,
url: String,
image: String,
}
#[tauri::command]
pub fn fetch_opg(url: String) -> Result<OpenGraphResponse, ()> {
let mut options = WebpageOptions::default();
options.allow_insecure = true;
options.max_redirections = 3;
options.timeout = Duration::from_secs(15);
let info = Webpage::from_url(&url, options).expect("Failed");
let html = info.html;
let result = OpenGraphResponse {
title: html
.opengraph
.properties
.get("title")
.cloned()
.unwrap_or_default(),
description: html
.opengraph
.properties
.get("description")
.cloned()
.unwrap_or_default(),
url: html
.opengraph
.properties
.get("url")
.cloned()
.unwrap_or_default(),
image: html
.opengraph
.images
.get(0)
.and_then(|i| Some(i.url.clone()))
.unwrap_or_default(),
};
Ok(result.into())
}

View File

@ -0,0 +1,25 @@
use keyring::Entry;
#[tauri::command]
pub fn secure_save(key: String, value: String) -> Result<(), ()> {
let entry = Entry::new("Lume", &key).expect("Failed to create entry");
let _ = entry.set_password(&value);
Ok(())
}
#[tauri::command]
pub fn secure_load(key: String) -> Result<String, String> {
let entry = Entry::new("Lume", &key).expect("Failed to create entry");
if let Ok(password) = entry.get_password() {
Ok(password.into())
} else {
Err("Not found".into())
}
}
#[tauri::command]
pub fn secure_remove(key: String) -> Result<(), ()> {
let entry = Entry::new("Lume", &key).expect("Failed to remove entry");
let _ = entry.delete_password();
Ok(())
}

View File

@ -3,7 +3,7 @@
windows_subsystem = "windows"
)]
mod commands;
pub mod commands;
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_sql::{Migration, MigrationKind};
@ -23,14 +23,12 @@ fn main() {
tauri_plugin_sql::Builder::default()
.add_migrations(
"sqlite:lume_v3.db",
vec![
Migration {
version: 20230418013219,
description: "initial data",
sql: include_str!("../migrations/20230418013219_initial_data.sql"),
kind: MigrationKind::Up,
},
],
vec![Migration {
version: 20230418013219,
description: "initial data",
sql: include_str!("../migrations/20230418013219_initial_data.sql"),
kind: MigrationKind::Up,
}],
)
.build(),
)
@ -50,11 +48,11 @@ fn main() {
Some(vec![]),
))
.invoke_handler(tauri::generate_handler![
commands::opengraph,
commands::secure_save,
commands::secure_load,
commands::secure_remove,
commands::show_in_folder,
commands::secret::secure_save,
commands::secret::secure_load,
commands::secret::secure_remove,
commands::folder::show_in_folder,
commands::opg::fetch_opg,
])
.run(ctx)
.expect("error while running tauri application");