mirror of
https://github.com/lumehq/lume.git
synced 2025-10-07 21:52:45 +02:00
chore: clean up
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"name": "lume",
|
||||
"description": "the communication app",
|
||||
"private": true,
|
||||
"version": "3.0.0",
|
||||
"scripts": {
|
||||
@@ -22,7 +21,6 @@
|
||||
"@lume/ui": "workspace:^",
|
||||
"@lume/utils": "workspace:^",
|
||||
"@nostr-dev-kit/ndk": "^2.3.2",
|
||||
"@nostr-fetch/adapter-ndk": "^0.14.1",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
@@ -32,7 +30,6 @@
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-toolbar": "^1.0.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"@tauri-apps/api": "2.0.0-alpha.13",
|
||||
@@ -48,25 +45,8 @@
|
||||
"@tauri-apps/plugin-sql": "2.0.0-alpha.5",
|
||||
"@tauri-apps/plugin-updater": "2.0.0-alpha.5",
|
||||
"@tauri-apps/plugin-upload": "2.0.0-alpha.5",
|
||||
"@tiptap/extension-character-count": "^2.1.13",
|
||||
"@tiptap/extension-document": "^2.1.13",
|
||||
"@tiptap/extension-image": "^2.1.13",
|
||||
"@tiptap/extension-mention": "^2.1.13",
|
||||
"@tiptap/extension-paragraph": "^2.1.13",
|
||||
"@tiptap/extension-placeholder": "^2.1.13",
|
||||
"@tiptap/extension-text": "^2.1.13",
|
||||
"@tiptap/pm": "^2.1.13",
|
||||
"@tiptap/react": "^2.1.13",
|
||||
"@tiptap/starter-kit": "^2.1.13",
|
||||
"@tiptap/suggestion": "^2.1.13",
|
||||
"@vidstack/react": "^1.9.8",
|
||||
"clsx": "^2.1.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"framer-motion": "^10.16.16",
|
||||
"html-to-text": "^9.0.5",
|
||||
"light-bolt11-decoder": "^3.0.0",
|
||||
"lru-cache": "^10.1.0",
|
||||
"markdown-to-jsx": "^7.4.0",
|
||||
"minidenticons": "^4.2.0",
|
||||
"nanoid": "^5.0.4",
|
||||
"nostr-fetch": "^0.14.1",
|
||||
@@ -76,20 +56,15 @@
|
||||
"react-currency-input-field": "^3.6.14",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.49.2",
|
||||
"react-hotkeys-hook": "^4.4.1",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"smol-toml": "^1.1.3",
|
||||
"sonner": "^1.3.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"tiptap-markdown": "^0.8.8",
|
||||
"virtua": "^0.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lume/tailwindcss": "workspace:^",
|
||||
"@lume/tsconfig": "workspace:^",
|
||||
"@lume/types": "workspace:^",
|
||||
"@types/html-to-text": "^9.0.4",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
@@ -99,8 +74,9 @@
|
||||
"encoding": "^0.1.13",
|
||||
"postcss": "^8.4.32",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^4.5.1",
|
||||
"vite": "^5.0.10",
|
||||
"vite-tsconfig-paths": "^4.2.3"
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 84 KiB |
Binary file not shown.
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 217 KiB |
@@ -14,7 +14,7 @@
|
||||
}
|
||||
|
||||
.prose :where(iframe):not(:where([class~='not-prose'] *)) {
|
||||
@apply mx-auto aspect-video h-auto w-full;
|
||||
@apply w-full h-auto mx-auto aspect-video;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,11 +42,3 @@ input::-ms-clear {
|
||||
.border {
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.ProseMirror p.is-empty::before {
|
||||
@apply pointer-events-none float-left h-0 text-neutral-600 content-[attr(data-placeholder)] dark:text-neutral-400;
|
||||
}
|
||||
|
||||
.ProseMirror img.ProseMirror-selectednode {
|
||||
@apply outline-blue-500;
|
||||
}
|
||||
|
@@ -1,12 +1,6 @@
|
||||
import { useStorage } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import {
|
||||
AppLayout,
|
||||
AuthLayout,
|
||||
ComposerLayout,
|
||||
HomeLayout,
|
||||
SettingsLayout,
|
||||
} from "@lume/ui";
|
||||
import { AppLayout, AuthLayout, HomeLayout, SettingsLayout } from "@lume/ui";
|
||||
import { fetch } from "@tauri-apps/plugin-http";
|
||||
import {
|
||||
RouterProvider,
|
||||
@@ -70,44 +64,6 @@ export default function App() {
|
||||
return { Component: RelayScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "new",
|
||||
element: <ComposerLayout />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
async lazy() {
|
||||
const { NewPostScreen } = await import("./routes/new/post");
|
||||
return { Component: NewPostScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "article",
|
||||
async lazy() {
|
||||
const { NewArticleScreen } = await import(
|
||||
"./routes/new/article"
|
||||
);
|
||||
return { Component: NewArticleScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "file",
|
||||
async lazy() {
|
||||
const { NewFileScreen } = await import("./routes/new/file");
|
||||
return { Component: NewFileScreen };
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "privkey",
|
||||
async lazy() {
|
||||
const { NewPrivkeyScreen } = await import(
|
||||
"./routes/new/privkey"
|
||||
);
|
||||
return { Component: NewPrivkeyScreen };
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
element: <SettingsLayout />,
|
||||
@@ -304,8 +260,8 @@ export default function App() {
|
||||
<RouterProvider
|
||||
router={router}
|
||||
fallbackElement={
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<LoaderIcon className="h-6 w-6 animate-spin" />
|
||||
<div className="flex items-center justify-center w-full h-full">
|
||||
<LoaderIcon className="w-6 h-6 animate-spin" />
|
||||
</div>
|
||||
}
|
||||
future={{ v7_startTransition: true }}
|
||||
|
@@ -84,7 +84,7 @@ export function HomeScreen() {
|
||||
{columns.map((column) => renderItem(column))}
|
||||
</VList>
|
||||
<div className="absolute bottom-3 right-3">
|
||||
<div className="flex items-center gap-1 p-1 bg-black/50 backdrop-blur-xl rounded-xl">
|
||||
<div className="flex items-center gap-1 p-1 bg-black/50 dark:bg-white/30 backdrop-blur-xl rounded-xl">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
|
@@ -1,314 +0,0 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import {
|
||||
BoldIcon,
|
||||
Heading1Icon,
|
||||
Heading2Icon,
|
||||
Heading3Icon,
|
||||
ItalicIcon,
|
||||
LoaderIcon,
|
||||
ThreadsIcon,
|
||||
} from "@lume/icons";
|
||||
import { NDKKind, NDKTag } from "@nostr-dev-kit/ndk";
|
||||
import CharacterCount from "@tiptap/extension-character-count";
|
||||
import Image from "@tiptap/extension-image";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { EditorContent, FloatingMenu, useEditor } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Markdown } from "tiptap-markdown";
|
||||
import {
|
||||
ArticleCoverUploader,
|
||||
MediaUploader,
|
||||
MentionPopup,
|
||||
} from "./components";
|
||||
|
||||
export function NewArticleScreen() {
|
||||
const ark = useArk();
|
||||
|
||||
const [height, setHeight] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [title, setTitle] = useState("");
|
||||
const [summary, setSummary] = useState({ open: false, content: "" });
|
||||
const [cover, setCover] = useState("");
|
||||
|
||||
const navigate = useNavigate();
|
||||
const containerRef = useRef(null);
|
||||
const ident = useMemo(() => String(Date.now()), []);
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure(),
|
||||
Placeholder.configure({ placeholder: "Type something..." }),
|
||||
Image.configure({
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"rounded-lg w-full object-cover h-auto max-h-[400px] border border-neutral-200 dark:border-neutral-800 outline outline-1 outline-offset-0 outline-neutral-300 dark:outline-neutral-700",
|
||||
},
|
||||
}),
|
||||
CharacterCount.configure(),
|
||||
Markdown.configure({
|
||||
html: false,
|
||||
tightLists: true,
|
||||
linkify: true,
|
||||
transformPastedText: true,
|
||||
}),
|
||||
],
|
||||
content: JSON.parse(localStorage.getItem("editor-post") || "{}"),
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class:
|
||||
"outline-none prose prose-lg prose-neutral max-w-none select-text whitespace-pre-line break-words dark:prose-invert hover:prose-a:text-blue-500",
|
||||
},
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
const jsonContent = JSON.stringify(editor.getJSON());
|
||||
localStorage.setItem("editor-article", jsonContent);
|
||||
},
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (!ark.ndk.signer) return navigate("/new/privkey");
|
||||
|
||||
setLoading(true);
|
||||
|
||||
// get markdown content
|
||||
const content = editor.storage.markdown.getMarkdown();
|
||||
|
||||
// define tags
|
||||
const tags: NDKTag[] = [
|
||||
["d", ident],
|
||||
["title", title],
|
||||
["image", cover],
|
||||
["summary", summary.content],
|
||||
["published_at", String(Math.floor(Date.now() / 1000))],
|
||||
];
|
||||
|
||||
// add hashtag to tags if present
|
||||
const hashtags = content
|
||||
.split(/\s/gm)
|
||||
.filter((s: string) => s.startsWith("#"));
|
||||
|
||||
if (hashtags) {
|
||||
for (const tag of hashtags) {
|
||||
tags.push(["t", tag.replace("#", "")]);
|
||||
}
|
||||
}
|
||||
|
||||
// publish
|
||||
const publish = await ark.createEvent({
|
||||
content,
|
||||
tags,
|
||||
kind: NDKKind.Article,
|
||||
});
|
||||
|
||||
if (publish) {
|
||||
toast.success(
|
||||
`Broadcasted to ${publish.seens.length} relays successfully.`,
|
||||
);
|
||||
|
||||
// update state
|
||||
setLoading(false);
|
||||
|
||||
// reset editor
|
||||
editor.commands.clearContent();
|
||||
localStorage.setItem("editor-article", "{}");
|
||||
}
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
toast.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setHeight(containerRef.current.clientHeight);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col justify-between">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div
|
||||
className="flex flex-col gap-4"
|
||||
ref={containerRef}
|
||||
style={{ height: `${height}px` }}
|
||||
>
|
||||
{cover ? (
|
||||
<img
|
||||
src={cover}
|
||||
alt="post cover"
|
||||
className="h-72 w-full rounded-lg object-cover"
|
||||
/>
|
||||
) : null}
|
||||
<div className="group flex justify-between gap-2">
|
||||
<input
|
||||
name="title"
|
||||
className="h-9 flex-1 border-none bg-transparent px-0 text-2xl font-semibold text-neutral-900 shadow-none outline-none placeholder:text-neutral-400 focus:border-none focus:outline-none focus:ring-0 dark:text-neutral-100 dark:placeholder:text-neutral-600"
|
||||
placeholder="Untitled"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
/>
|
||||
<div
|
||||
className={twMerge(
|
||||
"inline-flex shrink-0 gap-2 group-hover:inline-flex",
|
||||
title.length > 0 ? "" : "hidden",
|
||||
)}
|
||||
>
|
||||
<ArticleCoverUploader setCover={setCover} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setSummary((prev) => ({ ...prev, open: !prev.open }))
|
||||
}
|
||||
className="inline-flex h-9 w-max items-center gap-2 rounded-lg bg-neutral-100 px-2.5 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-800"
|
||||
>
|
||||
<ThreadsIcon className="h-4 w-4" />
|
||||
Add summary
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{summary.open ? (
|
||||
<div className="flex gap-3">
|
||||
<div className="h-16 w-1 shrink-0 rounded-full bg-neutral-200 dark:bg-neutral-800" />
|
||||
<div className="flex-1">
|
||||
<textarea
|
||||
className="h-16 w-full border-none bg-transparent px-1 py-1 text-neutral-900 shadow-none outline-none placeholder:text-neutral-400 dark:text-neutral-100 dark:placeholder:text-neutral-600"
|
||||
placeholder="A brief summary of your article"
|
||||
value={summary.content}
|
||||
onChange={(e) =>
|
||||
setSummary((prev) => ({ ...prev, content: e.target.value }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
{editor && (
|
||||
<FloatingMenu
|
||||
editor={editor}
|
||||
tippyOptions={{ duration: 100 }}
|
||||
className="ml-36 inline-flex h-10 items-center gap-1 rounded-lg border border-neutral-200 bg-neutral-100 px-px dark:border-neutral-800 dark:bg-neutral-900"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 1 }).run()
|
||||
}
|
||||
className={twMerge(
|
||||
"inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950",
|
||||
editor.isActive("heading", { level: 1 })
|
||||
? "bg-white shadow dark:bg-black"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<Heading1Icon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||
}
|
||||
className={twMerge(
|
||||
"inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950",
|
||||
editor.isActive("heading", { level: 2 })
|
||||
? "bg-white shadow dark:bg-black"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<Heading2Icon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||
}
|
||||
className={twMerge(
|
||||
"inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950",
|
||||
editor.isActive("heading", { level: 3 })
|
||||
? "bg-white shadow dark:bg-black"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<Heading3Icon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||
className={twMerge(
|
||||
"inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950",
|
||||
editor.isActive("bold")
|
||||
? "bg-white shadow dark:bg-black"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<BoldIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
className={twMerge(
|
||||
"inline-flex h-9 w-9 items-center justify-center rounded-md text-neutral-900 hover:bg-neutral-50 dark:text-neutral-100 dark:hover:bg-neutral-950",
|
||||
editor.isActive("italic")
|
||||
? "bg-white shadow dark:bg-black"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<ItalicIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</FloatingMenu>
|
||||
)}
|
||||
<EditorContent
|
||||
editor={editor}
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-3 flex h-12 w-full items-center rounded-lg bg-yellow-100 px-3 text-yellow-700">
|
||||
<p className="text-sm">
|
||||
Article editor is still in beta. If you need a stable and more
|
||||
reliable feature, you can use <b>Habla (habla.news)</b> instead.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex h-16 w-full items-center justify-between border-t border-neutral-100 dark:border-neutral-900">
|
||||
<div className="inline-flex items-center gap-3">
|
||||
<span className="text-sm font-medium tabular-nums text-neutral-600 dark:text-neutral-400">
|
||||
{editor?.storage?.characterCount.characters()} characters
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums text-neutral-600 dark:text-neutral-400">
|
||||
-
|
||||
</span>
|
||||
<span className="text-sm font-medium tabular-nums text-neutral-600 dark:text-neutral-400">
|
||||
<b>Identifier:</b>
|
||||
{ident}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<MediaUploader editor={editor} />
|
||||
<MentionPopup editor={editor} />
|
||||
</div>
|
||||
<div className="mx-3 h-6 w-px bg-neutral-200 dark:bg-neutral-800" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
disabled={editor?.isEmpty}
|
||||
className="inline-flex h-9 w-max items-center justify-center rounded-lg bg-blue-500 px-2.5 font-medium text-white hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{loading === true ? (
|
||||
<LoaderIcon className="h-5 w-5 animate-spin" />
|
||||
) : (
|
||||
"Publish article"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,86 +0,0 @@
|
||||
import { ImageIcon, LoaderIcon } from "@lume/icons";
|
||||
import { message, open } from "@tauri-apps/plugin-dialog";
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
import { useState } from "react";
|
||||
|
||||
export function ArticleCoverUploader({ setCover }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const uploadToNostrBuild = async () => {
|
||||
try {
|
||||
// start loading
|
||||
setLoading(true);
|
||||
|
||||
const selected = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: "Media",
|
||||
extensions: [
|
||||
"png",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"gif",
|
||||
"mp4",
|
||||
"mp3",
|
||||
"webm",
|
||||
"mkv",
|
||||
"avi",
|
||||
"mov",
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!selected) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await readFile(selected.path);
|
||||
const blob = new Blob([file]);
|
||||
|
||||
const data = new FormData();
|
||||
data.append("fileToUpload", blob);
|
||||
data.append("submit", "Upload Image");
|
||||
|
||||
const res = await fetch("https://nostr.build/api/v2/upload/files", {
|
||||
method: "POST",
|
||||
body: data,
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
const content = json.data[0];
|
||||
setCover(content.url);
|
||||
|
||||
// stop loading
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
// stop loading
|
||||
setLoading(false);
|
||||
await message(`Upload failed, error: ${e}`, {
|
||||
title: "Lume",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={uploadToNostrBuild}
|
||||
className="inline-flex h-9 w-max items-center justify-center gap-2 rounded-lg bg-neutral-100 px-2.5 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-800"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
Add cover
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
@@ -1,5 +0,0 @@
|
||||
export * from './articleCoverUploader';
|
||||
export * from './mediaUploader';
|
||||
export * from './mentionPopup';
|
||||
export * from './mentionPopupItem';
|
||||
export * from './mentionList';
|
@@ -1,46 +0,0 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { MediaIcon } from "@lume/icons";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { useState } from "react";
|
||||
|
||||
export function MediaUploader({ editor }: { editor: Editor }) {
|
||||
const ark = useArk();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const uploadToNostrBuild = async () => {
|
||||
try {
|
||||
// start loading
|
||||
setLoading(true);
|
||||
|
||||
const image = await ark.upload({
|
||||
fileExts: ["mp4", "mp3", "webm", "mkv", "avi", "mov"],
|
||||
});
|
||||
|
||||
if (image) {
|
||||
editor.commands.setImage({ src: image });
|
||||
editor.commands.createParagraphNear();
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
// stop loading
|
||||
setLoading(false);
|
||||
await message(`Upload failed, error: ${e}`, {
|
||||
title: "Lume",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => uploadToNostrBuild()}
|
||||
className="inline-flex h-9 w-max items-center justify-center gap-1.5 rounded-lg bg-neutral-100 px-2 text-sm font-medium text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
||||
>
|
||||
<MediaIcon className="h-5 w-5" />
|
||||
{loading ? "Uploading..." : "Add media"}
|
||||
</button>
|
||||
);
|
||||
}
|
@@ -1,115 +0,0 @@
|
||||
import { NDKCacheUserProfile } from "@lume/types";
|
||||
import * as Avatar from "@radix-ui/react-avatar";
|
||||
import { minidenticon } from "minidenticons";
|
||||
import {
|
||||
Ref,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
} from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type MentionListRef = {
|
||||
onKeyDown: (props: { event: Event }) => boolean;
|
||||
};
|
||||
|
||||
const List = (
|
||||
props: {
|
||||
items: NDKCacheUserProfile[];
|
||||
command: (arg0: { id: string }) => void;
|
||||
},
|
||||
ref: Ref<unknown>,
|
||||
) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = (index) => {
|
||||
const item = props.items[index];
|
||||
if (item) {
|
||||
props.command({ id: item.pubkey });
|
||||
}
|
||||
};
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex(
|
||||
(selectedIndex + props.items.length - 1) % props.items.length,
|
||||
);
|
||||
};
|
||||
|
||||
const downHandler = () => {
|
||||
setSelectedIndex((selectedIndex + 1) % props.items.length);
|
||||
};
|
||||
|
||||
const enterHandler = () => {
|
||||
selectItem(selectedIndex);
|
||||
};
|
||||
|
||||
useEffect(() => setSelectedIndex(0), [props.items]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onKeyDown: ({ event }) => {
|
||||
if (event.key === "ArrowUp") {
|
||||
upHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "ArrowDown") {
|
||||
downHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.key === "Enter") {
|
||||
enterHandler();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="flex w-[200px] flex-col overflow-y-auto rounded-lg border border-neutral-200 bg-neutral-50 p-2 shadow-lg shadow-neutral-500/20 dark:border-neutral-800 dark:bg-neutral-950 dark:shadow-neutral-300/50">
|
||||
{props.items.length ? (
|
||||
props.items.map((item, index) => (
|
||||
<button
|
||||
type="button"
|
||||
key={item.pubkey}
|
||||
onClick={() => selectItem(index)}
|
||||
className={twMerge(
|
||||
"inline-flex h-11 items-center gap-2 rounded-md px-2",
|
||||
index === selectedIndex
|
||||
? "bg-neutral-100 dark:bg-neutral-900"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
<Avatar.Root className="h-8 w-8 shrink-0">
|
||||
<Avatar.Image
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-8 w-8 rounded-md"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={150}>
|
||||
<img
|
||||
src={`data:image/svg+xml;utf8,${encodeURIComponent(
|
||||
minidenticon(item.name, 90, 50),
|
||||
)}`}
|
||||
alt={item.name}
|
||||
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<h5 className="max-w-[150px] truncate text-sm font-medium">
|
||||
{item.name}
|
||||
</h5>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center text-sm font-medium">No result</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const MentionList = forwardRef<MentionListRef>(List);
|
@@ -1,51 +0,0 @@
|
||||
import { useStorage } from "@lume/ark";
|
||||
import { MentionIcon } from "@lume/icons";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { MentionPopupItem } from "./mentionPopupItem";
|
||||
|
||||
export function MentionPopup({ editor }: { editor: Editor }) {
|
||||
const storage = useStorage();
|
||||
|
||||
const insertMention = (pubkey: string) => {
|
||||
editor.commands.insertContent(`nostr:${nip19.npubEncode(pubkey)}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-9 w-max items-center justify-center gap-1.5 rounded-lg bg-neutral-100 px-2 text-sm font-medium text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
||||
>
|
||||
<MentionIcon className="h-5 w-5" />
|
||||
Mention
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content
|
||||
side="top"
|
||||
sideOffset={5}
|
||||
className="h-full max-h-[200px] w-[250px] overflow-hidden overflow-y-auto rounded-lg border border-neutral-200 bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:bg-neutral-900"
|
||||
>
|
||||
<div className="flex flex-col gap-1 py-1">
|
||||
{storage.account.contacts.length ? (
|
||||
storage.account.contacts.map((item) => (
|
||||
<button
|
||||
key={item}
|
||||
type="button"
|
||||
onClick={() => insertMention(item)}
|
||||
>
|
||||
<MentionPopupItem pubkey={item} />
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="flex h-16 items-center justify-center">
|
||||
Contact list is empty
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
);
|
||||
}
|
@@ -1,57 +0,0 @@
|
||||
import { useProfile } from "@lume/ark";
|
||||
import { displayNpub } from "@lume/utils";
|
||||
import * as Avatar from "@radix-ui/react-avatar";
|
||||
import { minidenticon } from "minidenticons";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export function MentionPopupItem({ pubkey }: { pubkey: string }) {
|
||||
const { isLoading, user } = useProfile(pubkey);
|
||||
const svgURI = useMemo(
|
||||
() =>
|
||||
`data:image/svg+xml;utf8,${encodeURIComponent(
|
||||
minidenticon(pubkey, 90, 50),
|
||||
)}`,
|
||||
[pubkey],
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center gap-2.5 px-2">
|
||||
<div className="relative h-8 w-8 shrink-0 animate-pulse rounded bg-neutral-400 dark:bg-neutral-600" />
|
||||
<div className="flex w-full flex-1 flex-col items-start gap-1 text-start">
|
||||
<span className="h-4 w-1/2 animate-pulse rounded bg-neutral-400 dark:bg-neutral-600" />
|
||||
<span className="h-3 w-1/3 animate-pulse rounded bg-neutral-400 dark:bg-neutral-600" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-11 items-center justify-start gap-2.5 px-2 hover:bg-neutral-200 dark:bg-neutral-800">
|
||||
<Avatar.Root className="shirnk-0 h-8 w-8">
|
||||
<Avatar.Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-8 w-8 rounded-md object-cover"
|
||||
/>
|
||||
<Avatar.Fallback delayMs={300}>
|
||||
<img
|
||||
src={svgURI}
|
||||
alt={pubkey}
|
||||
className="h-8 w-8 rounded-md bg-black dark:bg-white"
|
||||
/>
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div className="flex flex-col items-start gap-px">
|
||||
<h5 className="max-w-[10rem] truncate text-sm font-medium leading-none text-neutral-900 dark:text-neutral-100">
|
||||
{user?.display_name || user?.displayName || user?.name}
|
||||
</h5>
|
||||
<span className="text-sm leading-none text-neutral-600 dark:text-neutral-400">
|
||||
{displayNpub(pubkey, 16)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,184 +0,0 @@
|
||||
import { useArk } from "@lume/ark";
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { message, open } from "@tauri-apps/plugin-dialog";
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function NewFileScreen() {
|
||||
const ark = useArk();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isPublish, setIsPublish] = useState(false);
|
||||
const [metadata, setMetadata] = useState<string[][] | null>(null);
|
||||
const [caption, setCaption] = useState("");
|
||||
|
||||
const uploadFile = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const selected = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: "Media",
|
||||
extensions: [
|
||||
"png",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"gif",
|
||||
"mp4",
|
||||
"mp3",
|
||||
"webm",
|
||||
"mkv",
|
||||
"avi",
|
||||
"mov",
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!selected) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const file = await readFile(selected.path);
|
||||
const blob = new Blob([file]);
|
||||
|
||||
const data = new FormData();
|
||||
data.append("fileToUpload", blob);
|
||||
data.append("submit", "Upload Image");
|
||||
|
||||
const res = await fetch("https://nostr.build/api/v2/upload/files", {
|
||||
method: "POST",
|
||||
body: data,
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const json = await res.json();
|
||||
const data = json.data[0];
|
||||
|
||||
setMetadata([
|
||||
["url", data.url],
|
||||
["m", data.mime ?? "application/octet-stream"],
|
||||
["x", data.sha256 ?? ""],
|
||||
["size", data.size.toString() ?? "0"],
|
||||
["dim", `${data.dimensions.width}x${data.dimensions.height}` ?? "0"],
|
||||
["blurhash", data.blurhash ?? ""],
|
||||
["thumb", data.thumbnail ?? ""],
|
||||
]);
|
||||
|
||||
// stop loading
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (e) {
|
||||
// stop loading
|
||||
setLoading(false);
|
||||
await message(`Upload failed, error: ${e}`, {
|
||||
title: "Lume",
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (!ark.ndk.signer) return navigate("/new/privkey");
|
||||
|
||||
setIsPublish(true);
|
||||
|
||||
const publish = await ark.createEvent({
|
||||
kind: 1063,
|
||||
tags: metadata,
|
||||
content: caption,
|
||||
});
|
||||
|
||||
if (publish) {
|
||||
toast.success(
|
||||
`Broadcasted to ${publish.seens.length} relays successfully.`,
|
||||
);
|
||||
setMetadata(null);
|
||||
setIsPublish(false);
|
||||
}
|
||||
} catch (e) {
|
||||
setIsPublish(false);
|
||||
toast.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full">
|
||||
<div className="flex h-96 gap-4 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||
<button
|
||||
type="button"
|
||||
onClick={uploadFile}
|
||||
className="flex h-full flex-1 flex-col items-center justify-center rounded-lg border border-dashed border-neutral-200 bg-neutral-50 p-2 hover:border-blue-500 hover:text-blue-500 dark:border-neutral-800 dark:bg-neutral-950"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-5 w-5 animate-spin text-neutral-900 dark:text-neutral-100" />
|
||||
) : !metadata ? (
|
||||
<div className="flex flex-col text-center">
|
||||
<h5 className="text-lg font-semibold">
|
||||
Click or drag a file to this area to upload
|
||||
</h5>
|
||||
<p className="text-sm font-medium text-neutral-600 dark:text-neutral-400">
|
||||
Supports: jpg, png, webp, gif, mov, mp4 or mp3
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<img
|
||||
src={metadata[0][1]}
|
||||
alt={metadata[1][1]}
|
||||
className="aspect-square h-full w-full rounded-lg object-cover shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
{metadata ? (
|
||||
<div className="flex h-full flex-1 flex-col justify-between">
|
||||
<div className="flex flex-col gap-2 py-2">
|
||||
{metadata.map((item, index) => (
|
||||
<div key={item[0] + index} className="flex min-w-0 gap-2">
|
||||
<h5 className="w-24 shrink-0 truncate font-semibold capitalize text-neutral-600 dark:text-neutral-400">
|
||||
{item[0]}
|
||||
</h5>
|
||||
<p className="w-72 truncate">{item[1]}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<input
|
||||
name="caption"
|
||||
type="text"
|
||||
value={caption}
|
||||
onChange={(e) => setCaption(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder="Caption (Optional)..."
|
||||
className="h-11 w-full rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-900 dark:placeholder:text-neutral-400"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
disabled={!metadata}
|
||||
className="inline-flex h-9 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{isPublish ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
"Share"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,173 +0,0 @@
|
||||
import { MentionNote, useArk, useSuggestion, useWidget } from "@lume/ark";
|
||||
import { CancelIcon, LoaderIcon } from "@lume/icons";
|
||||
import { COL_TYPES } from "@lume/utils";
|
||||
import { NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import CharacterCount from "@tiptap/extension-character-count";
|
||||
import Image from "@tiptap/extension-image";
|
||||
import Mention from "@tiptap/extension-mention";
|
||||
import Placeholder from "@tiptap/extension-placeholder";
|
||||
import { EditorContent, useEditor } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { convert } from "html-to-text";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { MediaUploader, MentionPopup } from "./components";
|
||||
|
||||
export function NewPostScreen() {
|
||||
const ark = useArk();
|
||||
const { addWidget } = useWidget();
|
||||
const { suggestion } = useSuggestion();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [height, setHeight] = useState(0);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const containerRef = useRef(null);
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure(),
|
||||
Placeholder.configure({ placeholder: "Sharing some thoughts..." }),
|
||||
Image.configure({
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"rounded-lg w-full object-cover h-auto max-h-[400px] border border-neutral-200 dark:border-neutral-800 outline outline-1 outline-offset-0 outline-neutral-300 dark:outline-neutral-700",
|
||||
},
|
||||
}),
|
||||
CharacterCount.configure(),
|
||||
Mention.configure({
|
||||
suggestion,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
renderLabel({ options, node }) {
|
||||
const npub = nip19.npubEncode(node.attrs.id);
|
||||
return `nostr:${npub}`;
|
||||
},
|
||||
}),
|
||||
],
|
||||
content: JSON.parse(localStorage.getItem("editor-post") || "{}"),
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class:
|
||||
"outline-none prose prose-lg prose-neutral max-w-none select-text whitespace-pre-line break-words dark:prose-invert hover:prose-a:text-blue-500",
|
||||
},
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
const jsonContent = JSON.stringify(editor.getJSON());
|
||||
localStorage.setItem("editor-post", jsonContent);
|
||||
},
|
||||
});
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
if (!ark.ndk.signer) return navigate("/new/privkey");
|
||||
|
||||
setLoading(true);
|
||||
|
||||
// get plaintext content
|
||||
const html = editor.getHTML();
|
||||
const serializedContent = convert(html, {
|
||||
selectors: [
|
||||
{ selector: "a", options: { linkBrackets: false } },
|
||||
{ selector: "img", options: { linkBrackets: false } },
|
||||
],
|
||||
});
|
||||
|
||||
// add reply to tags if present
|
||||
const replyTo = searchParams.get("replyTo");
|
||||
const rootReplyTo = searchParams.get("rootReplyTo");
|
||||
|
||||
// publish event
|
||||
const publish = await ark.createEvent({
|
||||
kind: NDKKind.Text,
|
||||
tags: [],
|
||||
content: serializedContent,
|
||||
replyTo,
|
||||
rootReplyTo,
|
||||
});
|
||||
|
||||
if (publish) {
|
||||
toast.success(
|
||||
`Broadcasted to ${publish.seens.length} relays successfully.`,
|
||||
);
|
||||
|
||||
// update state
|
||||
setLoading(false);
|
||||
setSearchParams({});
|
||||
|
||||
// open new widget with this event id
|
||||
if (!replyTo) {
|
||||
addWidget.mutate({
|
||||
title: "Thread",
|
||||
content: publish.id,
|
||||
kind: COL_TYPES.thread,
|
||||
});
|
||||
}
|
||||
|
||||
// reset editor
|
||||
editor.commands.clearContent();
|
||||
localStorage.setItem("editor-post", "{}");
|
||||
}
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
toast.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setHeight(containerRef.current.clientHeight);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex h-[500px] flex-1 flex-col gap-4">
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div ref={containerRef} style={{ height: `${height}px` }}>
|
||||
<EditorContent
|
||||
editor={editor}
|
||||
spellCheck="false"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
/>
|
||||
{searchParams.get("replyTo") && (
|
||||
<div className="relative max-w-lg">
|
||||
<MentionNote eventId={searchParams.get("replyTo")} />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSearchParams({})}
|
||||
className="absolute right-3 top-3 inline-flex h-6 w-6 items-center justify-center rounded bg-neutral-200 px-2 dark:bg-neutral-800"
|
||||
>
|
||||
<CancelIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="inline-flex h-16 w-full items-center justify-between border-t border-neutral-100 bg-neutral-50 dark:border-neutral-900 dark:bg-neutral-950">
|
||||
<span className="text-sm font-medium tabular-nums text-neutral-600 dark:text-neutral-400">
|
||||
{editor?.storage?.characterCount.characters()} characters
|
||||
</span>
|
||||
<div className="flex items-center">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<MediaUploader editor={editor} />
|
||||
<MentionPopup editor={editor} />
|
||||
</div>
|
||||
<div className="mx-3 h-6 w-px bg-neutral-200 dark:bg-neutral-800" />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
disabled={editor?.isEmpty}
|
||||
className="inline-flex h-9 w-20 items-center justify-center rounded-lg bg-blue-500 px-2 font-medium text-white hover:bg-blue-600 disabled:opacity-50"
|
||||
>
|
||||
{loading === true ? (
|
||||
<LoaderIcon className="h-5 w-5 animate-spin" />
|
||||
) : (
|
||||
"Post"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
import { useArk, useStorage } from "@lume/ark";
|
||||
import { NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { getPublicKey, nip19 } from "nostr-tools";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export function NewPrivkeyScreen() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [nsec, setNsec] = useState("");
|
||||
|
||||
const submit = async (isSave?: boolean) => {
|
||||
try {
|
||||
if (!nsec.startsWith("nsec1"))
|
||||
return toast.info("You must enter a private key starts with nsec");
|
||||
|
||||
const decoded = nip19.decode(nsec);
|
||||
|
||||
if (decoded.type !== "nsec")
|
||||
return toast.info("You must enter a valid nsec");
|
||||
|
||||
const privkey = decoded.data;
|
||||
const pubkey = getPublicKey(privkey);
|
||||
|
||||
if (pubkey !== storage.account.pubkey)
|
||||
return toast.info(
|
||||
"Your nsec is not match your current public key, please make sure you enter right nsec",
|
||||
);
|
||||
|
||||
const signer = new NDKPrivateKeySigner(privkey);
|
||||
ark.updateNostrSigner({ signer });
|
||||
|
||||
if (isSave) await storage.createPrivkey(storage.account.pubkey, privkey);
|
||||
|
||||
navigate(-1);
|
||||
} catch (e) {
|
||||
toast.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="mb-16 flex flex-col gap-3">
|
||||
<h1 className="text-center font-semibold text-neutral-900 dark:text-neutral-100">
|
||||
You need to provide private key to sign nostr event.
|
||||
</h1>
|
||||
<input
|
||||
name="privkey"
|
||||
placeholder="nsec..."
|
||||
type="password"
|
||||
value={nsec}
|
||||
onChange={(e) => setNsec(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
<div className="mt-2 flex flex-col gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit(true)}
|
||||
className="inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-neutral-100 font-medium text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
||||
>
|
||||
Submit and Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
import { useArk, useStorage } from "@lume/ark";
|
||||
import { CancelIcon, RefreshIcon } from "@lume/icons";
|
||||
import { useRelay } from "@lume/utils";
|
||||
import { NDKKind } from "@nostr-dev-kit/ndk";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { RelayForm } from "./relayForm";
|
||||
@@ -9,7 +8,6 @@ export function UserRelayList() {
|
||||
const ark = useArk();
|
||||
const storage = useStorage();
|
||||
|
||||
const { removeRelay } = useRelay();
|
||||
const { status, data, refetch } = useQuery({
|
||||
queryKey: ["relays", storage.account.pubkey],
|
||||
queryFn: async () => {
|
||||
@@ -32,21 +30,21 @@ export function UserRelayList() {
|
||||
|
||||
return (
|
||||
<div className="col-span-1">
|
||||
<div className="inline-flex h-16 w-full items-center justify-between border-b border-neutral-100 px-3 dark:border-neutral-900">
|
||||
<div className="inline-flex items-center justify-between w-full h-16 px-3 border-b border-neutral-100 dark:border-neutral-900">
|
||||
<h3 className="font-semibold">Connected relays</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => refetch()}
|
||||
className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md hover:bg-neutral-100 dark:hover:bg-neutral-900"
|
||||
className="inline-flex items-center justify-center w-6 h-6 rounded-md shrink-0 hover:bg-neutral-100 dark:hover:bg-neutral-900"
|
||||
>
|
||||
<RefreshIcon className="h-4 w-4" />
|
||||
<RefreshIcon className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-col gap-2 px-3">
|
||||
<div className="flex flex-col gap-2 px-3 mt-3">
|
||||
{status === "pending" ? (
|
||||
<p>Loading...</p>
|
||||
) : !data.length ? (
|
||||
<div className="flex h-20 w-full items-center justify-center rounded-xl bg-neutral-50 dark:bg-neutral-950">
|
||||
<div className="flex items-center justify-center w-full h-20 rounded-xl bg-neutral-50 dark:bg-neutral-950">
|
||||
<p className="text-sm font-medium">
|
||||
You not have personal relay list yet
|
||||
</p>
|
||||
@@ -55,18 +53,18 @@ export function UserRelayList() {
|
||||
data.map((item) => (
|
||||
<div
|
||||
key={item[1]}
|
||||
className="group flex h-11 items-center justify-between rounded-lg bg-neutral-100 px-3 dark:bg-neutral-900"
|
||||
className="flex items-center justify-between px-3 rounded-lg group h-11 bg-neutral-100 dark:bg-neutral-900"
|
||||
>
|
||||
<div className="inline-flex items-baseline gap-2">
|
||||
{currentRelays.has(item[1]) ? (
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-400 opacity-75" />
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-teal-500" />
|
||||
<span className="relative flex w-2 h-2">
|
||||
<span className="absolute inline-flex w-full h-full bg-green-400 rounded-full opacity-75 animate-ping" />
|
||||
<span className="relative inline-flex w-2 h-2 bg-teal-500 rounded-full" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75" />
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500" />
|
||||
<span className="relative flex w-2 h-2">
|
||||
<span className="absolute inline-flex w-full h-full bg-red-400 rounded-full opacity-75 animate-ping" />
|
||||
<span className="relative inline-flex w-2 h-2 bg-red-500 rounded-full" />
|
||||
</span>
|
||||
)}
|
||||
<p className="max-w-[20rem] truncate text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
||||
@@ -75,16 +73,15 @@ export function UserRelayList() {
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
{item[2]?.length ? (
|
||||
<div className="inline-flex h-6 w-max items-center justify-center rounded bg-neutral-200 px-2 text-xs font-medium capitalize dark:bg-neutral-800">
|
||||
<div className="inline-flex items-center justify-center h-6 px-2 text-xs font-medium capitalize rounded w-max bg-neutral-200 dark:bg-neutral-800">
|
||||
{item[2]}
|
||||
</div>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeRelay.mutate(item[1])}
|
||||
className="hidden h-6 w-6 items-center justify-center rounded group-hover:inline-flex hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
||||
className="items-center justify-center hidden w-6 h-6 rounded group-hover:inline-flex hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<CancelIcon className="h-4 w-4 text-neutral-900 dark:text-neutral-100" />
|
||||
<CancelIcon className="w-4 h-4 text-neutral-900 dark:text-neutral-100" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -44,7 +44,6 @@ export function ColumnHeader({
|
||||
return (
|
||||
<div className="flex items-center justify-between w-full px-3 border-b h-11 shrink-0 border-neutral-100 dark:border-neutral-900">
|
||||
<div className="inline-flex items-center gap-4">
|
||||
<div className="w-1 h-5 bg-blue-500 rounded-full shrink-0" />
|
||||
<div className="inline-flex items-center flex-1 gap-2 text-neutral-800 dark:text-neutral-200">
|
||||
{icon ? icon : <ThreadIcon className="size-4" />}
|
||||
<div className="text-sm font-medium">{title}</div>
|
||||
@@ -61,7 +60,7 @@ export function ColumnHeader({
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content className="flex w-[220px] flex-col overflow-hidden rounded-xl border border-neutral-100 bg-white p-2 shadow-lg shadow-neutral-200/50 focus:outline-none dark:border-neutral-900 dark:bg-neutral-950 dark:shadow-neutral-900/50">
|
||||
<DropdownMenu.Content className="flex w-[200px] p-2 flex-col overflow-hidden rounded-xl border border-neutral-100 bg-neutral-50 dark:bg-neutral-950 focus:outline-none dark:border-neutral-900">
|
||||
<DropdownMenu.Item asChild>
|
||||
<button
|
||||
type="button"
|
||||
|
@@ -34,13 +34,13 @@ export function ColumnLiveWidget({
|
||||
if (!events.length) return null;
|
||||
|
||||
return (
|
||||
<div className="absolute left-0 top-11 z-50 flex h-11 w-full items-center justify-center">
|
||||
<div className="absolute left-0 z-50 flex items-center justify-center w-full top-11 h-11">
|
||||
<button
|
||||
type="button"
|
||||
onClick={update}
|
||||
className="inline-flex h-9 w-max items-center justify-center gap-1 rounded-full bg-blue-500 px-2.5 text-sm font-semibold text-white hover:bg-blue-600"
|
||||
className="inline-flex items-center justify-center h-8 gap-1 pl-2 pr-2.5 text-sm font-semibold rounded-full w-max bg-neutral-950 dark:bg-neutral-50 hover:bg-neutral-900 dark:hover:bg-neutral-100 text-neutral-50 dark:text-neutral-950"
|
||||
>
|
||||
<ChevronUpIcon className="h-4 w-4" />
|
||||
<ChevronUpIcon className="w-4 h-4" />
|
||||
{events.length} {events.length === 1 ? "new event" : "new events"}
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { HorizontalDotsIcon } from "@lume/icons";
|
||||
import { HorizontalDotsIcon, ShareIcon } from "@lume/icons";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { nip19 } from "nostr-tools";
|
||||
@@ -40,9 +40,9 @@ export function NoteMenu() {
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-6 w-6 items-center justify-center"
|
||||
className="inline-flex items-center justify-center w-6 h-6"
|
||||
>
|
||||
<HorizontalDotsIcon className="h-4 w-4 text-neutral-800 hover:text-blue-500 dark:text-neutral-200" />
|
||||
<HorizontalDotsIcon className="w-4 h-4 text-neutral-800 hover:text-blue-500 dark:text-neutral-200" />
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
@@ -51,7 +51,7 @@ export function NoteMenu() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyLink()}
|
||||
className="inline-flex h-10 items-center px-4 text-sm text-white hover:bg-neutral-900 rounded-lg focus:outline-none"
|
||||
className="inline-flex items-center h-10 px-4 text-sm text-white rounded-lg hover:bg-neutral-900 focus:outline-none"
|
||||
>
|
||||
Copy shareable link
|
||||
</button>
|
||||
@@ -60,7 +60,7 @@ export function NoteMenu() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyID()}
|
||||
className="inline-flex h-10 items-center px-4 text-sm text-white hover:bg-neutral-900 rounded-lg focus:outline-none"
|
||||
className="inline-flex items-center h-10 px-4 text-sm text-white rounded-lg hover:bg-neutral-900 focus:outline-none"
|
||||
>
|
||||
Copy note ID
|
||||
</button>
|
||||
@@ -69,7 +69,7 @@ export function NoteMenu() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => copyRaw()}
|
||||
className="inline-flex h-10 items-center px-4 text-sm text-white hover:bg-neutral-900 rounded-lg focus:outline-none"
|
||||
className="inline-flex items-center h-10 px-4 text-sm text-white rounded-lg hover:bg-neutral-900 focus:outline-none"
|
||||
>
|
||||
Copy raw event
|
||||
</button>
|
||||
@@ -77,7 +77,7 @@ export function NoteMenu() {
|
||||
<DropdownMenu.Item asChild>
|
||||
<Link
|
||||
to={`/users/${event.pubkey}`}
|
||||
className="inline-flex h-10 items-center px-4 text-sm text-white hover:bg-neutral-900 rounded-lg focus:outline-none"
|
||||
className="inline-flex items-center h-10 px-4 text-sm text-white rounded-lg hover:bg-neutral-900 focus:outline-none"
|
||||
>
|
||||
View profile
|
||||
</Link>
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import { PinIcon } from "@lume/icons";
|
||||
import { COL_TYPES } from "@lume/utils";
|
||||
import { Link } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Note } from ".";
|
||||
import { useColumnContext } from "../column";
|
||||
|
||||
export function NoteThread({
|
||||
thread,
|
||||
@@ -9,23 +12,40 @@ export function NoteThread({
|
||||
thread: { rootEventId: string; replyEventId: string };
|
||||
className?: string;
|
||||
}) {
|
||||
const { addColumn } = useColumnContext();
|
||||
|
||||
if (!thread) return null;
|
||||
|
||||
return (
|
||||
<div className={twMerge("w-full px-3", className)}>
|
||||
<div className="flex h-min w-full flex-col gap-3 rounded-lg bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||
<div className="flex flex-col w-full gap-3 p-3 rounded-lg h-min bg-neutral-100 dark:bg-neutral-900">
|
||||
{thread.rootEventId ? (
|
||||
<Note.Child eventId={thread.rootEventId} isRoot />
|
||||
) : null}
|
||||
{thread.replyEventId ? (
|
||||
<Note.Child eventId={thread.replyEventId} />
|
||||
) : null}
|
||||
<Link
|
||||
to={`/events/${thread?.rootEventId || thread?.replyEventId}`}
|
||||
className="self-start text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
Show thread
|
||||
</Link>
|
||||
<div className="inline-flex items-center justify-between">
|
||||
<Link
|
||||
to={`/events/${thread?.rootEventId || thread?.replyEventId}`}
|
||||
className="self-start text-blue-500 hover:text-blue-600"
|
||||
>
|
||||
Show thread
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () =>
|
||||
await addColumn({
|
||||
kind: COL_TYPES.thread,
|
||||
title: "Thread",
|
||||
content: thread?.rootEventId || thread?.replyEventId,
|
||||
})
|
||||
}
|
||||
className="inline-flex items-center justify-center rounded-md text-neutral-600 dark:text-neutral-400 size-6 bg-neutral-200 dark:bg-neutral-800 hover:bg-neutral-300 dark:hover:bg-neutral-700"
|
||||
>
|
||||
<PinIcon className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { LoaderIcon } from "@lume/icons";
|
||||
import { NDKCacheAdapterTauri } from "@lume/ndk-cache-tauri";
|
||||
import { LumeStorage } from "@lume/storage";
|
||||
import { QUOTES, delay } from "@lume/utils";
|
||||
import { QUOTES, delay, sendNativeNotification } from "@lume/utils";
|
||||
import NDK, {
|
||||
NDKNip46Signer,
|
||||
NDKPrivateKeySigner,
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
normalizeRelayUrlSet,
|
||||
} from "nostr-fetch";
|
||||
import { PropsWithChildren, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { createContext, useContextSelector } from "use-context-selector";
|
||||
import { Ark } from "./ark";
|
||||
|
||||
@@ -161,7 +160,9 @@ const LumeProvider = ({ children }: PropsWithChildren<object>) => {
|
||||
const signIn = NDKRelayAuthPolicies.signIn({ ndk, signer });
|
||||
const event = await signIn(relay, challenge);
|
||||
if (event) {
|
||||
toast.success(`You've sign in sucessfully to relay: ${relay.url}`);
|
||||
sendNativeNotification(
|
||||
`You've sign in sucessfully to relay: ${relay.url}`,
|
||||
);
|
||||
return event;
|
||||
}
|
||||
};
|
||||
|
@@ -1,21 +1,24 @@
|
||||
import { SVGProps } from 'react';
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export function ShareIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M22.085 11.628l-8.501-7.63a.5.5 0 00-.834.373V8.5C4.25 8.5 2 11.75 2 20.25c1.5-3 2.25-4.75 10.75-4.75v4.129a.5.5 0 00.834.372l8.501-7.63a.5.5 0 000-.744z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
export function ShareIcon(
|
||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||
) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M15.41 6.51c-2.583.773-4.925 2.033-6.82 3.98m6.82 7c-2.583-.773-4.925-2.033-6.82-3.98M21 5a3 3 0 11-6 0 3 3 0 016 0zM9 12a3 3 0 11-6 0 3 3 0 016 0zm12 7a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
@@ -7,8 +7,10 @@
|
||||
"@lume/ark": "workspace:^",
|
||||
"@lume/icons": "workspace:^",
|
||||
"@lume/utils": "workspace:^",
|
||||
"@nostr-dev-kit/ndk": "^2.3.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
@@ -16,6 +18,7 @@
|
||||
"@tauri-apps/plugin-http": "2.0.0-alpha.6",
|
||||
"@tauri-apps/plugin-os": "2.0.0-alpha.6",
|
||||
"minidenticons": "^4.2.0",
|
||||
"nostr-tools": "1.17",
|
||||
"react": "^18.2.0",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"sonner": "^1.3.1"
|
||||
|
@@ -7,7 +7,6 @@ export * from "./user";
|
||||
export * from "./titlebar";
|
||||
export * from "./layouts/app";
|
||||
export * from "./layouts/auth";
|
||||
export * from "./layouts/composer";
|
||||
export * from "./layouts/home";
|
||||
export * from "./layouts/settings";
|
||||
export * from "./mentions";
|
||||
|
@@ -1,52 +0,0 @@
|
||||
import { NavLink, Outlet, useLocation } from 'react-router-dom';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function ComposerLayout() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="container mx-auto h-full px-8 pt-8">
|
||||
<div className="mb-8 flex h-10 shrink-0 items-center gap-3">
|
||||
{location.pathname !== '/new/privkey' ? (
|
||||
<div className="flex h-10 items-center gap-2 rounded-lg bg-neutral-100 px-0.5 dark:bg-neutral-800">
|
||||
<NavLink
|
||||
to="/new/"
|
||||
end
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
'inline-flex h-9 w-20 items-center justify-center rounded-lg text-sm font-medium',
|
||||
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
|
||||
)
|
||||
}
|
||||
>
|
||||
Post
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/new/article"
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
'inline-flex h-9 w-20 items-center justify-center rounded-lg text-sm font-medium',
|
||||
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
|
||||
)
|
||||
}
|
||||
>
|
||||
Article
|
||||
</NavLink>
|
||||
<NavLink
|
||||
to="/new/file"
|
||||
className={({ isActive }) =>
|
||||
twMerge(
|
||||
'inline-flex h-9 w-28 items-center justify-center rounded-lg text-sm font-medium',
|
||||
isActive ? 'bg-white shadow dark:bg-black' : 'bg-transparent'
|
||||
)
|
||||
}
|
||||
>
|
||||
File Sharing
|
||||
</NavLink>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -5,19 +5,17 @@ import {
|
||||
HomeIcon,
|
||||
NwcFilledIcon,
|
||||
NwcIcon,
|
||||
PlusIcon,
|
||||
RelayFilledIcon,
|
||||
RelayIcon,
|
||||
SearchIcon,
|
||||
} from "@lume/icons";
|
||||
import { cn } from "@lume/utils";
|
||||
import { Link, NavLink } from "react-router-dom";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { ActiveAccount } from "./account/active";
|
||||
|
||||
export function Navigation() {
|
||||
return (
|
||||
<div className="flex h-full w-20 shrink-0 flex-col justify-between px-4 py-3">
|
||||
<div className="flex flex-1 flex-col gap-5">
|
||||
<div className="flex flex-col justify-between w-20 h-full px-4 py-3 shrink-0">
|
||||
<div className="flex flex-col flex-1 gap-5">
|
||||
<NavLink
|
||||
to="/"
|
||||
preventScrollReset={true}
|
||||
@@ -34,7 +32,7 @@ export function Navigation() {
|
||||
)}
|
||||
>
|
||||
{isActive ? (
|
||||
<HomeFilledIcon className="size-6 text-black dark:text-white" />
|
||||
<HomeFilledIcon className="text-black size-6 dark:text-white" />
|
||||
) : (
|
||||
<HomeIcon className="size-6" />
|
||||
)}
|
||||
@@ -102,7 +100,7 @@ export function Navigation() {
|
||||
)}
|
||||
>
|
||||
{isActive ? (
|
||||
<DepotFilledIcon className="size-6 text-black dark:text-white" />
|
||||
<DepotFilledIcon className="text-black size-6 dark:text-white" />
|
||||
) : (
|
||||
<DepotIcon className="size-6" />
|
||||
)}
|
||||
@@ -136,7 +134,7 @@ export function Navigation() {
|
||||
)}
|
||||
>
|
||||
{isActive ? (
|
||||
<NwcFilledIcon className="size-6 text-black dark:text-white" />
|
||||
<NwcFilledIcon className="text-black size-6 dark:text-white" />
|
||||
) : (
|
||||
<NwcIcon className="size-6" />
|
||||
)}
|
||||
@@ -155,19 +153,7 @@ export function Navigation() {
|
||||
)}
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="flex shrink-0 flex-col gap-3 p-1">
|
||||
<Link
|
||||
to="/new/"
|
||||
className="flex aspect-square h-auto w-full items-center justify-center rounded-xl bg-black/10 text-black hover:bg-blue-500 hover:text-white dark:bg-white/10 dark:text-white dark:hover:bg-blue-500"
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
</Link>
|
||||
<Link
|
||||
to="/nwc"
|
||||
className="flex aspect-square h-auto w-full items-center justify-center rounded-xl bg-black/10 hover:bg-blue-500 hover:text-white dark:bg-white/10 dark:hover:bg-blue-500"
|
||||
>
|
||||
<SearchIcon className="h-5 w-5" />
|
||||
</Link>
|
||||
<div className="flex flex-col gap-3 p-1 shrink-0">
|
||||
<ActiveAccount />
|
||||
</div>
|
||||
</div>
|
||||
|
841
pnpm-lock.yaml
generated
841
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user