feat: add editor screen

This commit is contained in:
2024-02-23 14:56:24 +07:00
parent 64286aa354
commit 84584a4d1f
39 changed files with 917 additions and 493 deletions

View File

@@ -7,7 +7,6 @@
"@getalby/sdk": "^3.3.0",
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
"@lume/storage": "workspace:^",
"@lume/utils": "workspace:^",
"@nostr-dev-kit/ndk": "^2.4.1",
"@radix-ui/react-accordion": "^1.1.2",

View File

@@ -1,89 +1,80 @@
import {
ChevronDownIcon,
MoveLeftIcon,
MoveRightIcon,
RefreshIcon,
TrashIcon,
ChevronDownIcon,
MoveLeftIcon,
MoveRightIcon,
RefreshIcon,
TrashIcon,
} from "@lume/icons";
import { useColumn } from "@lume/storage";
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
import { useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { useColumnContext } from "./provider";
export function ColumnHeader({
queryKey,
}: {
queryKey?: string[];
}) {
const { t } = useTranslation();
const { move, remove } = useColumn();
export function ColumnHeader({ queryKey }: { queryKey?: string[] }) {
const { t } = useTranslation();
const queryClient = useQueryClient();
const column = useColumnContext();
const queryClient = useQueryClient();
const refresh = async () => {
if (queryKey) await queryClient.refetchQueries({ queryKey });
};
const refresh = async () => {
if (queryKey) await queryClient.refetchQueries({ queryKey });
};
return (
<DropdownMenu.Root>
<div className="flex items-center justify-center gap-2 px-3 w-full border-b h-11 shrink-0 border-neutral-100 dark:border-neutral-900">
<DropdownMenu.Trigger asChild>
<div className="inline-flex items-center gap-1.5">
<div className="text-[13px] font-medium">{column.title}</div>
<ChevronDownIcon className="size-5" />
</div>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
sideOffset={5}
className="flex w-[200px] p-2 flex-col overflow-hidden rounded-2xl bg-white/50 dark:bg-black/50 ring-1 ring-black/10 dark:ring-white/10 backdrop-blur-2xl focus:outline-none"
>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={refresh}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<RefreshIcon className="size-4" />
{t("global.refresh")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => move(column.id, "left")}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<MoveLeftIcon className="size-4" />
{t("global.moveLeft")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => move(column.id, "right")}
className="inline-flex items-center gap-3 px-3 text-sm font-medium rounded-lg h-9 text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<MoveRightIcon className="size-4" />
{t("global.moveRight")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Separator className="h-px my-1 bg-black/10 dark:bg-white/10" />
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => remove(column.id)}
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")}
</button>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</div>
</DropdownMenu.Root>
);
return (
<DropdownMenu.Root>
<div className="flex h-11 w-full shrink-0 items-center justify-center gap-2 border-b border-neutral-100 px-3 dark:border-neutral-900">
<DropdownMenu.Trigger asChild>
<div className="inline-flex items-center gap-1.5">
<div className="text-[13px] font-medium">{column.title}</div>
<ChevronDownIcon className="size-5" />
</div>
</DropdownMenu.Trigger>
<DropdownMenu.Portal>
<DropdownMenu.Content
sideOffset={5}
className="flex w-[200px] flex-col overflow-hidden rounded-2xl bg-white/50 p-2 ring-1 ring-black/10 backdrop-blur-2xl focus:outline-none dark:bg-black/50 dark:ring-white/10"
>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={refresh}
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<RefreshIcon className="size-4" />
{t("global.refresh")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => move(column.id, "left")}
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<MoveLeftIcon className="size-4" />
{t("global.moveLeft")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => move(column.id, "right")}
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
>
<MoveRightIcon className="size-4" />
{t("global.moveRight")}
</button>
</DropdownMenu.Item>
<DropdownMenu.Separator className="my-1 h-px bg-black/10 dark:bg-white/10" />
<DropdownMenu.Item asChild>
<button
type="button"
onClick={() => remove(column.id)}
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-red-500 hover:bg-red-500 hover:text-red-50 focus:outline-none"
>
<TrashIcon className="size-4" />
{t("global.delete")}
</button>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</div>
</DropdownMenu.Root>
);
}

View File

@@ -1,6 +1,4 @@
import { useArk } from "@lume/ark";
import { LoaderIcon, TrashIcon } from "@lume/icons";
import { useStorage } from "@lume/storage";
import { cn, editorValueAtom } from "@lume/utils";
import { invoke } from "@tauri-apps/api/core";
import { useAtom } from "jotai";

View File

@@ -1,4 +1,3 @@
import { NDKCacheUserProfile } from "@lume/types";
import { ReactNode } from "react";
import ReactDOM from "react-dom";
import { BaseEditor, Transforms } from "slate";

View File

@@ -21,12 +21,10 @@ export function NoteChild({
const richContent = useMemo(() => {
if (!data) return "";
let parsedContent: string | ReactNode[] = data.content.replace(
/\n+/g,
"\n",
);
let parsedContent: string | ReactNode[] =
data.content.substring(0, 160) + "...";
const text = parsedContent as string;
const text = data.content;
const words = text.split(/( |\n)/);
const hashtags = words.filter((word) => word.startsWith("#"));
@@ -104,7 +102,7 @@ export function NoteChild({
<div className="relative flex gap-3">
<div className="relative flex-1 rounded-md bg-neutral-200 px-2 py-2 dark:bg-neutral-800">
<div className="absolute right-0 top-[18px] h-3 w-3 -translate-y-1/2 translate-x-1/2 rotate-45 transform bg-neutral-200 dark:bg-neutral-800" />
<div className="content-break mt-6 line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
<div className="content-break mt-6 select-text leading-normal text-neutral-900 dark:text-neutral-100">
{richContent}
</div>
</div>

View File

@@ -103,9 +103,9 @@ export function MentionNote({
}
return (
<div className="my-1.5 flex w-full cursor-default flex-col rounded-xl bg-neutral-100 pt-1 ring-1 ring-black/5 dark:bg-neutral-900 dark:ring-white/5">
<div className="my-1 flex w-full cursor-default flex-col rounded-xl bg-neutral-100 px-3 pt-1 ring-1 ring-black/5 dark:bg-neutral-900 dark:ring-white/5">
<User.Provider pubkey={data.pubkey}>
<User.Root className="flex h-10 items-center gap-2 px-3">
<User.Root className="flex h-10 items-center gap-2">
<User.Avatar className="size-6 shrink-0 rounded-full object-cover" />
<div className="inline-flex flex-1 items-center gap-2">
<User.Name className="font-semibold text-neutral-900 dark:text-neutral-100" />
@@ -117,11 +117,11 @@ export function MentionNote({
</div>
</User.Root>
</User.Provider>
<div className="line-clamp-4 select-text whitespace-normal text-balance px-3 leading-normal">
<div className="line-clamp-4 select-text whitespace-normal text-balance leading-normal">
{richContent}
</div>
{openable ? (
<div className="flex h-10 items-center justify-between px-3">
<div className="flex h-10 items-center justify-between">
<button
type="button"
onClick={() => ark.open_thread(data.id)}

View File

@@ -37,7 +37,7 @@ export function ImagePreview({ url }: { url: string }) {
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
<div
onClick={open}
className="group relative my-1.5 rounded-xl ring-1 ring-black/5 dark:ring-white/5"
className="group relative my-1 rounded-xl ring-1 ring-black/5 dark:ring-white/5"
>
<img
src={url}

View File

@@ -54,7 +54,7 @@ export function LinkPreview({ url }: { url: string }) {
href={url}
target="_blank"
rel="noreferrer"
className="my-1.5 flex w-full flex-col overflow-hidden rounded-xl bg-neutral-100 ring-1 ring-black/5 dark:bg-neutral-900 dark:ring-white/5"
className="my-1 flex w-full flex-col overflow-hidden rounded-xl bg-neutral-100 ring-1 ring-black/5 dark:bg-neutral-900 dark:ring-white/5"
>
{isImage(data.image) ? (
<img

View File

@@ -9,7 +9,7 @@ import {
export function VideoPreview({ url }: { url: string }) {
return (
<div className="my-1.5 w-full overflow-hidden rounded-xl ring-1 ring-black/5 dark:ring-white/5">
<div className="my-1 w-full overflow-hidden rounded-xl ring-1 ring-black/5 dark:ring-white/5">
<MediaController>
<video
slot="media"

View File

@@ -1,55 +0,0 @@
import { useArk } from "@lume/ark";
import { LoaderIcon } from "@lume/icons";
import { cn } from "@lume/utils";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { ReplyForm } from "./editor/replyForm";
import { Reply } from "./note/primitives/reply";
import { EventWithReplies } from "@lume/types";
export function ReplyList({
eventId,
className,
}: {
eventId: string;
className?: string;
}) {
const ark = useArk();
const [t] = useTranslation();
const [data, setData] = useState<null | EventWithReplies[]>(null);
useEffect(() => {
async function getReplies() {
const events = await ark.get_event_thread(eventId);
setData(events);
}
getReplies();
}, [eventId]);
return (
<div
className={cn(
"flex flex-col divide-y divide-neutral-100 dark:divide-neutral-900",
className,
)}
>
{!data ? (
<div className="mt-4 flex h-16 items-center justify-center p-3">
<LoaderIcon className="h-5 w-5 animate-spin" />
</div>
) : data.length === 0 ? (
<div className="mt-4 flex w-full items-center justify-center">
<div className="flex flex-col items-center justify-center gap-2 py-6">
<h3 className="text-3xl">👋</h3>
<p className="leading-none text-neutral-600 dark:text-neutral-400">
{t("note.reply.empty")}
</p>
</div>
</div>
) : (
data.map((event) => <Reply key={event.id} event={event} />)
)}
</div>
);
}

View File

@@ -1,7 +1,6 @@
import { ArrowLeftIcon, ArrowRightIcon } from "@lume/icons";
import { useNavigate, useParams } from "react-router-dom";
import { WindowVirtualizer } from "virtua";
import { ReplyList } from "../replyList";
import { ThreadNote } from "../note/primitives/thread";
export function EventRoute() {