This commit is contained in:
Ren Amamiya 2023-06-26 10:24:12 +07:00
parent 6af0b453e3
commit 7fb62a6afa
21 changed files with 479 additions and 374 deletions

View File

@ -27,6 +27,7 @@ export function ChatsList() {
return ( return (
<div className="flex flex-col"> <div className="flex flex-col">
<NewMessageModal />
{account ? ( {account ? (
<ChatsListSelfItem data={account} /> <ChatsListSelfItem data={account} />
) : ( ) : (
@ -59,7 +60,6 @@ export function ChatsList() {
<div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" /> <div className="h-3 w-full rounded-sm animate-pulse bg-zinc-800" />
</div> </div>
)} )}
<NewMessageModal />
</div> </div>
); );
} }

View File

@ -10,7 +10,7 @@ import { useNavigate } from "react-router-dom";
export function NewMessageModal() { export function NewMessageModal() {
const navigate = useNavigate(); const navigate = useNavigate();
const { status, data, isFetching }: any = useQuery(["plebs"], async () => { const { status, data }: any = useQuery(["plebs"], async () => {
return await getPlebs(); return await getPlebs();
}); });
@ -96,7 +96,7 @@ export function NewMessageModal() {
</div> </div>
</div> </div>
<div className="h-[500px] flex flex-col pb-5 overflow-x-hidden overflow-y-auto"> <div className="h-[500px] flex flex-col pb-5 overflow-x-hidden overflow-y-auto">
{status === "loading" || isFetching ? ( {status === "loading" ? (
<p>Loading...</p> <p>Loading...</p>
) : ( ) : (
data.map((pleb) => ( data.map((pleb) => (
@ -111,10 +111,10 @@ export function NewMessageModal() {
className="w-9 h-9 shrink-0 object-cover rounded" className="w-9 h-9 shrink-0 object-cover rounded"
/> />
<div className="inline-flex flex-col gap-1"> <div className="inline-flex flex-col gap-1">
<h3 className="leading-none max-w-[15rem] font-medium text-zinc-100"> <h3 className="leading-none max-w-[15rem] line-clamp-1 font-medium text-zinc-100">
{pleb.display_name || pleb.name} {pleb.display_name || pleb.name}
</h3> </h3>
<span className="leading-none max-w-[10rem] text-sm text-zinc-400"> <span className="leading-none max-w-[10rem] line-clamp-1 text-sm text-zinc-400">
{pleb.nip05 || {pleb.nip05 ||
pleb.npub.substring(0, 16).concat("...")} pleb.npub.substring(0, 16).concat("...")}
</span> </span>

View File

@ -144,8 +144,8 @@ export function Root() {
const notes = await fetchNotes(); const notes = await fetchNotes();
if (notes) { if (notes) {
const chats = await fetchChats(); const chats = await fetchChats();
const channels = await fetchChannelMessages(); // const channels = await fetchChannelMessages();
if (chats && channels) { if (chats) {
navigate("/app/space", { replace: true }); navigate("/app/space", { replace: true });
} }
} }

View File

@ -1,66 +1,11 @@
import { AddFeedBlock } from "@app/space/components/addFeed"; import { AddFeedBlock } from "@app/space/components/addFeed";
import { AddImageBlock } from "@app/space/components/addImage"; import { AddImageBlock } from "@app/space/components/addImage";
import { Menu, Transition } from "@headlessui/react";
import { FeedIcon, ImageIcon, PlusIcon } from "@shared/icons";
import { Fragment, useState } from "react";
export function AddBlock() { export function AddBlock() {
const [imageModal, setImageModal] = useState(false);
const [feedModal, setFeedModal] = useState(false);
const openAddImageModal = () => {
setImageModal(true);
};
const openAddFeedModal = () => {
setFeedModal(true);
};
return ( return (
<> <div className="flex flex-col gap-1">
<Menu as="div" className="relative inline-block text-left"> <AddImageBlock />
<Menu.Button className="group inline-flex flex-col items-center gap-2.5"> <AddFeedBlock />
<div className="inline-flex h-9 w-9 shrink items-center justify-center rounded-lg bg-zinc-900 group-hover:bg-zinc-800"> </div>
<PlusIcon width={16} height={16} className="text-zinc-500" />
</div>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute mt-2 right-1/2 transform translate-x-1/2 w-56 origin-top-right rounded-md bg-zinc-900/80 backdrop-blur-md focus:outline-none">
<div className="px-1 py-1">
<Menu.Item>
<button
type="button"
onClick={() => openAddImageModal()}
className="group flex w-full items-center rounded-md hover:bg-zinc-700/50 text-zinc-300 hover:text-zinc-100 px-2 py-2 text-sm"
>
<ImageIcon width={15} height={15} className="mr-2" />
Add image
</button>
</Menu.Item>
<Menu.Item>
<button
type="button"
onClick={() => openAddFeedModal()}
className="group flex w-full items-center rounded-md hover:bg-zinc-700/50 text-zinc-300 hover:text-zinc-100 px-2 py-2 text-sm"
>
<FeedIcon width={15} height={15} className="mr-2" />
Add feed
</button>
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
{imageModal && <AddImageBlock parentState={setImageModal} />}
{feedModal && <AddFeedBlock parentState={setFeedModal} />}
</>
); );
} }

View File

@ -1,24 +1,37 @@
import { User } from "@app/auth/components/user";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { createBlock } from "@libs/storage"; import { Combobox } from "@headlessui/react";
import { CancelIcon } from "@shared/icons"; import { createBlock, getPlebs } from "@libs/storage";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { CancelIcon, CheckCircleIcon, CommandIcon } from "@shared/icons";
import { DEFAULT_AVATAR } from "@stores/constants";
import { ADD_FEEDBLOCK_SHORTCUT } from "@stores/shortcuts";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useAccount } from "@utils/hooks/useAccount";
import { nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { Fragment, useState } from "react"; import { Fragment, useEffect, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useHotkeys } from "react-hotkeys-hook";
export function AddFeedBlock({ parentState }: { parentState: any }) { export function AddFeedBlock() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(true); const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState([]);
const [query, setQuery] = useState("");
const { status, account } = useAccount();
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => { const closeModal = () => {
// update local state
setIsOpen(false); setIsOpen(false);
// update parent state
parentState(false);
}; };
useHotkeys(ADD_FEEDBLOCK_SHORTCUT, () => openModal());
const block = useMutation({ const block = useMutation({
mutationFn: (data: any) => { mutationFn: (data: any) => {
return createBlock(data.kind, data.title, data.content); return createBlock(data.kind, data.title, data.content);
@ -38,151 +51,224 @@ export function AddFeedBlock({ parentState }: { parentState: any }) {
const onSubmit = (data: any) => { const onSubmit = (data: any) => {
setLoading(true); setLoading(true);
let pubkey = data.content; selected.forEach((item, index) => {
if (item.substring(0, 4) === "npub") {
if (pubkey.substring(0, 4) === "npub") { selected[index] = nip19.decode(item).data;
pubkey = nip19.decode(pubkey).data; }
} });
// insert to database // insert to database
block.mutate({ kind: 1, title: data.title, content: pubkey }); block.mutate({
kind: 1,
title: data.title,
content: JSON.stringify(selected),
});
setTimeout(() => { setLoading(false);
setLoading(false); // reset form
// reset form reset();
reset(); // close modal
// close modal closeModal();
closeModal();
}, 1200);
}; };
return ( return (
<Transition appear show={isOpen} as={Fragment}> <>
<Dialog as="div" className="relative z-50" onClose={closeModal}> <button
<Transition.Child type="button"
as={Fragment} onClick={() => openModal()}
enter="ease-out duration-300" className="inline-flex w-56 h-9 items-center justify-start gap-2.5 rounded-md px-2.5"
enterFrom="opacity-0" >
enterTo="opacity-100" <div className="flex items-center gap-2">
leave="ease-in duration-200" <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
leaveFrom="opacity-100" <CommandIcon width={12} height={12} className="text-zinc-500" />
leaveTo="opacity-0" </div>
> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" /> <span className="text-zinc-500 text-sm leading-none">F</span>
</Transition.Child> </div>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center"> </div>
<div>
<h5 className="font-medium text-zinc-400">New feed block</h5>
</div>
</button>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={closeModal}>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0 scale-95" enterFrom="opacity-0"
enterTo="opacity-100 scale-100" enterTo="opacity-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0"
> >
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-xl border-t border-zinc-800/50 bg-zinc-900"> <div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5"> </Transition.Child>
<div className="flex flex-col gap-2"> <div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="flex items-center justify-between"> <Transition.Child
<Dialog.Title as={Fragment}
as="h3" enter="ease-out duration-300"
className="text-lg font-semibold leading-none text-zinc-100" enterFrom="opacity-0 scale-95"
> enterTo="opacity-100 scale-100"
Create image block leave="ease-in duration-200"
</Dialog.Title> leaveFrom="opacity-100 scale-100"
<button leaveTo="opacity-0 scale-95"
type="button" >
onClick={closeModal} <Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-xl border-t border-zinc-800/50 bg-zinc-900">
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" <div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
> <div className="flex flex-col gap-2">
<CancelIcon <div className="flex items-center justify-between">
width={14} <Dialog.Title
height={14} as="h3"
className="text-zinc-300" className="text-lg font-semibold leading-none text-zinc-100"
/> >
</button> Create feed block
</Dialog.Title>
<button
type="button"
onClick={closeModal}
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
>
<CancelIcon
width={14}
height={14}
className="text-zinc-300"
/>
</button>
</div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
Specific newsfeed space for people you want to keep up to
date
</Dialog.Description>
</div> </div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
Pin your favorite image to Space then you can view every
time that you use Lume, your image will be broadcast to
Nostr Relay as well
</Dialog.Description>
</div> </div>
</div> <div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3"> <form
<form onSubmit={handleSubmit(onSubmit)}
onSubmit={handleSubmit(onSubmit)} className="flex h-full w-full flex-col gap-4 mb-0"
className="flex h-full w-full flex-col gap-4 mb-0" >
> <div className="flex flex-col gap-1">
<div className="flex flex-col gap-1"> <label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400"> Title *
Title * </label>
</label>
<div className="relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[6px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
<input <input
type={"text"} type={"text"}
{...register("title", { {...register("title", {
required: true, required: true,
})} })}
spellCheck={false} spellCheck={false}
className="relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500" className="relative h-10 w-full rounded-md px-3 py-2 !outline-none placeholder:text-zinc-500 bg-zinc-800 text-zinc-100"
/> />
</div> </div>
</div> <div className="flex flex-col gap-1">
<div className="flex flex-col gap-1"> <label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400"> Choose at least 1 user *
Pubkey OR Npub * </label>
</label> <div className="w-full h-[300px] flex flex-col rounded-lg border-t border-zinc-700/50 bg-zinc-800 overflow-x-hidden overflow-y-auto">
<div className="relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[6px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20"> <div className="w-full px-3 py-2">
<input <Combobox
type={"text"} value={selected}
{...register("content", { onChange={setSelected}
required: true, multiple
})} >
spellCheck={false} <Combobox.Input
className="relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500" onChange={(event) => setQuery(event.target.value)}
/> spellCheck={false}
autoFocus={false}
placeholder="Enter pubkey or npub..."
className="mb-2 relative h-10 w-full rounded-md px-3 py-2 !outline-none placeholder:text-zinc-500 bg-zinc-700 text-zinc-100"
/>
<Combobox.Options static>
{query.length > 0 && (
<Combobox.Option
value={query}
className="group w-full flex items-center justify-between px-2 py-2 rounded-md hover:bg-zinc-700"
>
{({ selected }) => (
<>
<div className="flex items-center gap-2">
<img
alt={query}
src={DEFAULT_AVATAR}
className="w-11 h-11 shrink-0 object-cover rounded"
/>
<div className="inline-flex flex-col gap-1">
<span className="text-base leading-tight text-zinc-400">
{query}
</span>
</div>
</div>
{selected && (
<CheckCircleIcon className="w-4 h-4 text-green-500" />
)}
</>
)}
</Combobox.Option>
)}
{status === "loading" ? (
<p>Loading...</p>
) : (
JSON.parse(account.follows).map((follow) => (
<Combobox.Option
key={follow}
value={follow}
className="group w-full flex items-center justify-between px-2 py-2 rounded-md hover:bg-zinc-700"
>
{({ selected }) => (
<>
<User pubkey={follow} />
{selected && (
<CheckCircleIcon className="w-4 h-4 text-green-500" />
)}
</>
)}
</Combobox.Option>
))
)}
</Combobox.Options>
</Combobox>
</div>
</div>
</div> </div>
</div> <div>
<div> <button
<button type="submit"
type="submit" disabled={!isDirty || !isValid}
disabled={!isDirty || !isValid} className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 shadow-button active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 shadow-button active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30" >
> {loading ? (
{loading ? ( <svg
<svg className="h-4 w-4 animate-spin text-black dark:text-zinc-100"
className="h-4 w-4 animate-spin text-black dark:text-zinc-100" xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" fill="none"
fill="none" viewBox="0 0 24 24"
viewBox="0 0 24 24" >
> <title id="loading">Loading</title>
<title id="loading">Loading</title> <circle
<circle className="opacity-25"
className="opacity-25" cx="12"
cx="12" cy="12"
cy="12" r="10"
r="10" stroke="currentColor"
stroke="currentColor" strokeWidth="4"
strokeWidth="4" />
/> <path
<path className="opacity-75"
className="opacity-75" fill="currentColor"
fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
/> </svg>
</svg> ) : (
) : ( "Confirm"
"Confirm" )}
)} </button>
</button> </div>
</div> </form>
</form> </div>
</div> </Dialog.Panel>
</Dialog.Panel> </Transition.Child>
</Transition.Child> </div>
</div> </Dialog>
</Dialog> </Transition>
</Transition> </>
); );
} }

View File

@ -1,10 +1,11 @@
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { createBlock } from "@libs/storage"; import { createBlock } from "@libs/storage";
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
import { CancelIcon } from "@shared/icons"; import { CancelIcon, CommandIcon } from "@shared/icons";
import { Image } from "@shared/image"; import { Image } from "@shared/image";
import { RelayContext } from "@shared/relayProvider"; import { RelayContext } from "@shared/relayProvider";
import { DEFAULT_AVATAR } from "@stores/constants"; import { DEFAULT_AVATAR } from "@stores/constants";
import { ADD_IMAGEBLOCK_SHORTCUT } from "@stores/shortcuts";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { open } from "@tauri-apps/api/dialog"; import { open } from "@tauri-apps/api/dialog";
import { Body, fetch } from "@tauri-apps/api/http"; import { Body, fetch } from "@tauri-apps/api/http";
@ -13,26 +14,30 @@ import { dateToUnix } from "@utils/date";
import { useAccount } from "@utils/hooks/useAccount"; import { useAccount } from "@utils/hooks/useAccount";
import { Fragment, useContext, useEffect, useRef, useState } from "react"; import { Fragment, useContext, useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useHotkeys } from "react-hotkeys-hook";
export function AddImageBlock({ parentState }: { parentState: any }) { export function AddImageBlock() {
const ndk = useContext(RelayContext); const ndk = useContext(RelayContext);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(true); const [isOpen, setIsOpen] = useState(false);
const [image, setImage] = useState(""); const [image, setImage] = useState("");
const { account } = useAccount(); const { account } = useAccount();
const tags = useRef(null); const tags = useRef(null);
const closeModal = () => { const openModal = () => {
// update local state setIsOpen(true);
setIsOpen(false);
// update parent state
parentState(false);
}; };
const closeModal = () => {
setIsOpen(false);
};
useHotkeys(ADD_IMAGEBLOCK_SHORTCUT, () => openModal());
const { const {
register, register,
handleSubmit, handleSubmit,
@ -118,13 +123,11 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
// mutate // mutate
block.mutate({ kind: 0, title: data.title, content: data.content }); block.mutate({ kind: 0, title: data.title, content: data.content });
setTimeout(() => { setLoading(false);
setLoading(false); // reset form
// reset form reset();
reset(); // close modal
// close modal closeModal();
closeModal();
}, 1200);
}; };
useEffect(() => { useEffect(() => {
@ -132,145 +135,164 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
}, [setValue, image]); }, [setValue, image]);
return ( return (
<Transition appear show={isOpen} as={Fragment}> <>
<Dialog as="div" className="relative z-50" onClose={closeModal}> <button
<Transition.Child type="button"
as={Fragment} onClick={() => openModal()}
enter="ease-out duration-300" className="inline-flex w-56 h-9 items-center justify-start gap-2.5 rounded-md px-2.5"
enterFrom="opacity-0" >
enterTo="opacity-100" <div className="flex items-center gap-2">
leave="ease-in duration-200" <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
leaveFrom="opacity-100" <CommandIcon width={12} height={12} className="text-zinc-500" />
leaveTo="opacity-0" </div>
> <div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" /> <span className="text-zinc-500 text-sm leading-none">I</span>
</Transition.Child> </div>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center"> </div>
<div>
<h5 className="font-medium text-zinc-400">New image block</h5>
</div>
</button>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={closeModal}>
<Transition.Child <Transition.Child
as={Fragment} as={Fragment}
enter="ease-out duration-300" enter="ease-out duration-300"
enterFrom="opacity-0 scale-95" enterFrom="opacity-0"
enterTo="opacity-100 scale-100" enterTo="opacity-100"
leave="ease-in duration-200" leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100"
leaveTo="opacity-0 scale-95" leaveTo="opacity-0"
> >
<Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-xl border-t border-zinc-800/50 bg-zinc-900"> <div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
<div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5"> </Transition.Child>
<div className="flex flex-col gap-2"> <div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<div className="flex items-center justify-between"> <Transition.Child
<Dialog.Title as={Fragment}
as="h3" enter="ease-out duration-300"
className="text-lg font-semibold leading-none text-zinc-100" enterFrom="opacity-0 scale-95"
> enterTo="opacity-100 scale-100"
Create image block leave="ease-in duration-200"
</Dialog.Title> leaveFrom="opacity-100 scale-100"
<button leaveTo="opacity-0 scale-95"
type="button" >
onClick={closeModal} <Dialog.Panel className="relative flex h-min w-full max-w-lg flex-col gap-2 rounded-xl border-t border-zinc-800/50 bg-zinc-900">
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900" <div className="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
> <div className="flex flex-col gap-2">
<CancelIcon <div className="flex items-center justify-between">
width={14} <Dialog.Title
height={14} as="h3"
className="text-zinc-300" className="text-lg font-semibold leading-none text-zinc-100"
/> >
</button> Create image block
</div> </Dialog.Title>
<Dialog.Description className="text-sm leading-tight text-zinc-400"> <button
Pin your favorite image to Space then you can view every type="button"
time that you use Lume, your image will be broadcast to onClick={closeModal}
Nostr Relay as well className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
</Dialog.Description> >
</div> <CancelIcon
</div> width={14}
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3"> height={14}
<form className="text-zinc-300"
onSubmit={handleSubmit(onSubmit)} />
className="flex h-full w-full flex-col gap-4 mb-0" </button>
>
<input
type={"hidden"}
{...register("content")}
value={image}
className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
<div className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
Title *
</label>
<div className="relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[6px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
<input
type={"text"}
{...register("title", {
required: true,
})}
spellCheck={false}
className="relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div> </div>
<Dialog.Description className="text-sm leading-tight text-zinc-400">
Pin your favorite image to Space then you can view every
time that you use Lume, your image will be broadcast to
Nostr Relay as well
</Dialog.Description>
</div> </div>
<div className="flex flex-col gap-1"> </div>
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400"> <div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
Picture <form
</label> onSubmit={handleSubmit(onSubmit)}
<div className="relative inline-flex h-56 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950"> className="flex h-full w-full flex-col gap-4 mb-0"
<Image >
src={image} <input
fallback={DEFAULT_AVATAR} type={"hidden"}
alt="content" {...register("content")}
className="relative z-10 max-h-[156px] h-auto w-[150px] object-cover rounded-md" value={image}
/> className="relative h-10 w-full rounded-lg border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
<div className="absolute bottom-3 right-3 z-10"> />
<button <div className="flex flex-col gap-1">
onClick={() => openFileDialog()} <label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
type="button" Title *
className="inline-flex h-6 items-center justify-center rounded bg-zinc-900 px-3 text-sm font-medium text-zinc-300 ring-1 ring-zinc-800 hover:bg-zinc-800" </label>
> <div className="relative w-full shrink-0 overflow-hidden before:pointer-events-none before:absolute before:-inset-1 before:rounded-[6px] before:border before:border-fuchsia-500 before:opacity-0 before:ring-2 before:ring-fuchsia-500/20 before:transition after:pointer-events-none after:absolute after:inset-px after:rounded-[6px] after:shadow-highlight after:shadow-white/5 after:transition focus-within:before:opacity-100 focus-within:after:shadow-fuchsia-500/100 dark:focus-within:after:shadow-fuchsia-500/20">
Upload <input
</button> type={"text"}
{...register("title", {
required: true,
})}
spellCheck={false}
className="relative h-10 w-full rounded-md border border-black/5 px-3 py-2 shadow-input shadow-black/5 !outline-none placeholder:text-zinc-400 dark:bg-zinc-800 dark:text-zinc-100 dark:shadow-black/10 dark:placeholder:text-zinc-500"
/>
</div> </div>
</div> </div>
</div> <div className="flex flex-col gap-1">
<div> <label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
<button Picture
type="submit" </label>
disabled={!isDirty || !isValid} <div className="relative inline-flex h-56 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 shadow-button active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30" <Image
> src={image}
{loading ? ( fallback={DEFAULT_AVATAR}
<svg alt="content"
className="h-4 w-4 animate-spin text-black dark:text-zinc-100" className="relative z-10 max-h-[156px] h-auto w-[150px] object-cover rounded-md"
xmlns="http://www.w3.org/2000/svg" />
fill="none" <div className="absolute bottom-3 right-3 z-10">
viewBox="0 0 24 24" <button
> onClick={() => openFileDialog()}
<title id="loading">Loading</title> type="button"
<circle className="inline-flex h-6 items-center justify-center rounded bg-zinc-900 px-3 text-sm font-medium text-zinc-300 ring-1 ring-zinc-800 hover:bg-zinc-800"
className="opacity-25" >
cx="12" Upload
cy="12" </button>
r="10" </div>
stroke="currentColor" </div>
strokeWidth="4" </div>
/> <div>
<path <button
className="opacity-75" type="submit"
fill="currentColor" disabled={!isDirty || !isValid}
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" className="inline-flex h-11 w-full transform items-center justify-center rounded-lg bg-fuchsia-500 font-medium text-zinc-100 shadow-button active:translate-y-1 disabled:cursor-not-allowed disabled:opacity-30"
/> >
</svg> {loading ? (
) : ( <svg
"Confirm" className="h-4 w-4 animate-spin text-black dark:text-zinc-100"
)} xmlns="http://www.w3.org/2000/svg"
</button> fill="none"
</div> viewBox="0 0 24 24"
</form> >
</div> <title id="loading">Loading</title>
</Dialog.Panel> <circle
</Transition.Child> className="opacity-25"
</div> cx="12"
</Dialog> cy="12"
</Transition> r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
) : (
"Confirm"
)}
</button>
</div>
</form>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition>
</>
); );
} }

View File

@ -1,4 +1,4 @@
import { getNotesByAuthor, removeBlock } from "@libs/storage"; import { getNotesByAuthors, removeBlock } from "@libs/storage";
import { Note } from "@shared/notes/note"; import { Note } from "@shared/notes/note";
import { NoteSkeleton } from "@shared/notes/skeleton"; import { NoteSkeleton } from "@shared/notes/skeleton";
import { TitleBar } from "@shared/titleBar"; import { TitleBar } from "@shared/titleBar";
@ -25,7 +25,7 @@ export function FeedBlock({ params }: { params: any }) {
}: any = useInfiniteQuery({ }: any = useInfiniteQuery({
queryKey: ["newsfeed", params.content], queryKey: ["newsfeed", params.content],
queryFn: async ({ pageParam = 0 }) => { queryFn: async ({ pageParam = 0 }) => {
return await getNotesByAuthor( return await getNotesByAuthors(
params.content, params.content,
TIME, TIME,
ITEM_PER_PAGE, ITEM_PER_PAGE,
@ -91,9 +91,9 @@ export function FeedBlock({ params }: { params: any }) {
className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto" className="scrollbar-hide flex w-full h-full flex-col justify-between gap-1.5 pt-1.5 pb-20 overflow-y-auto"
style={{ contain: "strict" }} style={{ contain: "strict" }}
> >
{status === "loading" || isFetching ? ( {status === "loading" ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-md border border-zinc-800 bg-zinc-900 px-3 py-3 shadow-input shadow-black/20"> <div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>
@ -119,6 +119,13 @@ export function FeedBlock({ params }: { params: any }) {
</div> </div>
</div> </div>
)} )}
{isFetching && !isFetchingNextPage && (
<div className="px-3 py-1.5">
<div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
<NoteSkeleton />
</div>
</div>
)}
</div> </div>
</div> </div>
); );

View File

@ -112,7 +112,7 @@ export function FollowingBlock({ block }: { block: number }) {
> >
{status === "loading" ? ( {status === "loading" ? (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20"> <div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>
@ -140,7 +140,7 @@ export function FollowingBlock({ block }: { block: number }) {
)} )}
{isFetching && !isFetchingNextPage && ( {isFetching && !isFetchingNextPage && (
<div className="px-3 py-1.5"> <div className="px-3 py-1.5">
<div className="rounded-md bg-zinc-900 px-3 py-3 shadow-input shadow-black/20"> <div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
<NoteSkeleton /> <NoteSkeleton />
</div> </div>
</div> </div>

View File

@ -21,7 +21,10 @@ export function ImageBlock({ params }: { params: any }) {
<div className="shrink-0 w-[350px] h-full flex flex-col justify-between border-r border-zinc-900"> <div className="shrink-0 w-[350px] h-full flex flex-col justify-between border-r border-zinc-900">
<div className="relative flex-1 w-full h-full p-3 overflow-hidden"> <div className="relative flex-1 w-full h-full p-3 overflow-hidden">
<div className="absolute top-3 left-0 w-full h-16 px-3"> <div className="absolute top-3 left-0 w-full h-16 px-3">
<div className="h-16 rounded-t-xl overflow-hidden flex items-center justify-end px-5"> <div className="h-16 rounded-t-xl overflow-hidden flex items-center justify-between px-5">
<h3 className="text-white font-medium drop-shadow-lg">
{params.title}
</h3>
<button <button
type="button" type="button"
onClick={() => block.mutate(params.id)} onClick={() => block.mutate(params.id)}

View File

@ -65,12 +65,12 @@ export function SpaceScreen() {
</div> </div>
</div> </div>
)} )}
<div className="shrink-0 w-[90px]"> <div className="shrink-0 w-[350px] flex-col flex border-r border-zinc-900">
<div className="w-full h-full inline-flex items-center justify-center"> <div className="w-full h-full inline-flex items-center justify-center">
<AddBlock /> <AddBlock />
</div> </div>
</div> </div>
<div className="shrink-0 w-[360px]" /> <div className="shrink-0 w-[350px]" />
</div> </div>
); );
} }

View File

@ -135,12 +135,14 @@ export async function countTotalNotes() {
const result = await db.select( const result = await db.select(
'SELECT COUNT(*) AS "total" FROM notes WHERE kind IN (1, 6);', 'SELECT COUNT(*) AS "total" FROM notes WHERE kind IN (1, 6);',
); );
return result[0].total; return parseInt(result[0].total);
} }
// get all notes // get all notes
export async function getNotes(time: number, limit: number, offset: number) { export async function getNotes(time: number, limit: number, offset: number) {
const db = await connect(); const db = await connect();
const totalNotes = await countTotalNotes();
const nextCursor = offset + limit;
const notes: any = { data: null, nextCursor: 0 }; const notes: any = { data: null, nextCursor: 0 };
const query: any = await db.select( const query: any = await db.select(
@ -148,19 +150,22 @@ export async function getNotes(time: number, limit: number, offset: number) {
); );
notes["data"] = query; notes["data"] = query;
notes["nextCursor"] = offset + limit; notes["nextCursor"] =
Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
return notes; return notes;
} }
// get all notes by authors // get all notes by pubkey
export async function getNotesByAuthor( export async function getNotesByPubkey(
pubkey: string, pubkey: string,
time: number, time: number,
limit: number, limit: number,
offset: number, offset: number,
) { ) {
const db = await connect(); const db = await connect();
const totalNotes = await countTotalNotes();
const nextCursor = offset + limit;
const notes: any = { data: null, nextCursor: 0 }; const notes: any = { data: null, nextCursor: 0 };
const query: any = await db.select( const query: any = await db.select(
@ -168,7 +173,33 @@ export async function getNotesByAuthor(
); );
notes["data"] = query; notes["data"] = query;
notes["nextCursor"] = offset + limit; notes["nextCursor"] =
Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
return notes;
}
// get all notes by authors
export async function getNotesByAuthors(
authors: string,
time: number,
limit: number,
offset: number,
) {
const db = await connect();
const totalNotes = await countTotalNotes();
const nextCursor = offset + limit;
const array = JSON.parse(authors);
const finalArray = `'${array.join("','")}'`;
const notes: any = { data: null, nextCursor: 0 };
const query: any = await db.select(
`SELECT * FROM notes WHERE created_at <= "${time}" AND pubkey IN (${finalArray}) AND kind IN (1, 6, 1063) GROUP BY parent_id ORDER BY created_at DESC LIMIT "${limit}" OFFSET "${offset}";`,
);
notes["data"] = query;
notes["nextCursor"] =
Math.round(totalNotes / nextCursor) > 1 ? nextCursor : undefined;
return notes; return notes;
} }

View File

@ -64,7 +64,7 @@ export function Navigation({ reverse = false }: { reverse?: boolean }) {
</NavLink> </NavLink>
</div> </div>
</div> </div>
{/* Channels */} {/* Channels
<Disclosure defaultOpen={true}> <Disclosure defaultOpen={true}>
{({ open }) => ( {({ open }) => (
<div className="flex flex-col gap-0.5 px-1.5"> <div className="flex flex-col gap-0.5 px-1.5">
@ -90,6 +90,7 @@ export function Navigation({ reverse = false }: { reverse?: boolean }) {
</div> </div>
)} )}
</Disclosure> </Disclosure>
*/}
{/* Chats */} {/* Chats */}
<Disclosure defaultOpen={true}> <Disclosure defaultOpen={true}>
{({ open }) => ( {({ open }) => (

View File

@ -53,7 +53,7 @@ export function Note({ event, block }: Note) {
Lume isn't fully support this kind in newsfeed Lume isn't fully support this kind in newsfeed
</p> </p>
</div> </div>
<div className="markdown"> <div className="select-text whitespace-pre-line break-words text-base text-zinc-100">
<p>{event.content}</p> <p>{event.content}</p>
</div> </div>
</div> </div>
@ -63,7 +63,7 @@ export function Note({ event, block }: Note) {
return ( return (
<div className="h-min w-full px-3 py-1.5"> <div className="h-min w-full px-3 py-1.5">
<div className="rounded-md bg-zinc-900 px-5 pt-5"> <div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
{renderParent} {renderParent}
<div className="flex flex-col"> <div className="flex flex-col">
<User <User

View File

@ -32,7 +32,7 @@ export function NoteParent({
Lume isn't fully support this kind in newsfeed Lume isn't fully support this kind in newsfeed
</p> </p>
</div> </div>
<div className="markdown"> <div className="select-text whitespace-pre-line break-words text-base text-zinc-100">
<p>{data.content || data.toString()}</p> <p>{data.content || data.toString()}</p>
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@ export function ImagePreview({ urls }: { urls: string[] }) {
src={url} src={url}
fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW" fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
alt="image" alt="image"
className="h-auto w-full rounded-lg object-cover" className="h-auto w-full border border-zinc-800/50 rounded-lg object-cover"
/> />
</div> </div>
))} ))}

View File

@ -20,7 +20,7 @@ export function LinkPreview({ urls }: { urls: string[] }) {
</div> </div>
) : ( ) : (
<a <a
className="flex flex-col rounded-lg border border-transparent hover:border-fuchsia-900" className="flex flex-col rounded-lg border border-zinc-800/50 hover:border-fuchsia-900"
href={urls[0]} href={urls[0]}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"

View File

@ -4,9 +4,14 @@ export function VideoPreview({ urls }: { urls: string[] }) {
return ( return (
<div className="relative mt-3 max-w-[420px] flex w-full flex-col gap-2"> <div className="relative mt-3 max-w-[420px] flex w-full flex-col gap-2">
{urls.map((url) => ( {urls.map((url) => (
<div key={url} className="aspect-video"> <ReactPlayer
<ReactPlayer url={url} width="100%" height="100%" /> key={url}
</div> url={url}
width="100%"
className="w-full h-auto border border-zinc-800/50 rounded-lg"
controls={true}
pip={true}
/>
))} ))}
</div> </div>
); );

View File

@ -29,8 +29,10 @@ export function RepliesList({ parent_id }: { parent_id: string }) {
<div className="px=3"> <div className="px=3">
<div className="w-full flex items-center justify-center rounded-md bg-zinc-900"> <div className="w-full flex items-center justify-center rounded-md bg-zinc-900">
<div className="py-6 flex flex-col items-center justify-center gap-2"> <div className="py-6 flex flex-col items-center justify-center gap-2">
<EmptyIcon width={56} height={56} /> <h3 className="text-3xl">👋</h3>
<p className="text-zinc-500 text-sm font-medium">No replies</p> <p className="leading-none text-zinc-400">
Share your thought on it...
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -15,7 +15,8 @@ export function Repost({
const { status, data, isFetching } = useEvent(repostID); const { status, data, isFetching } = useEvent(repostID);
return ( return (
<div className="relative overflow-hidden flex flex-col mt-12"> <div className="relative flex flex-col mt-12">
<div className="absolute left-[18px] -top-10 h-[50px] w-0.5 bg-gradient-to-t from-zinc-800 to-zinc-600" />
{isFetching || status === "loading" ? ( {isFetching || status === "loading" ? (
<NoteSkeleton /> <NoteSkeleton />
) : ( ) : (
@ -34,7 +35,7 @@ export function Repost({
Lume isn't fully support this kind in newsfeed Lume isn't fully support this kind in newsfeed
</p> </p>
</div> </div>
<div className="markdown"> <div className="select-text whitespace-pre-line break-words text-base text-zinc-100">
<p>{data.content || data.toString()}</p> <p>{data.content || data.toString()}</p>
</div> </div>
</div> </div>

View File

@ -33,14 +33,14 @@ export function User({
}`} }`}
> >
<Popover.Button <Popover.Button
className={`${avatarWidth} ${avatarHeight} shrink-0 overflow-hidden`} className={`${avatarWidth} ${avatarHeight} relative z-10 bg-zinc-900 shrink-0 overflow-hidden`}
> >
<Image <Image
src={user?.image} src={user?.image}
fallback={DEFAULT_AVATAR} fallback={DEFAULT_AVATAR}
alt={pubkey} alt={pubkey}
className={`${avatarWidth} ${avatarHeight} ${ className={`${avatarWidth} ${avatarHeight} ${
size === "small" ? "rounded" : "rounded-md" size === "small" ? "rounded" : "rounded-lg"
} object-cover`} } object-cover`}
/> />
</Popover.Button> </Popover.Button>

View File

@ -1 +1,3 @@
export const COMPOSE_SHORTCUT = "meta+n"; export const COMPOSE_SHORTCUT = "meta+n";
export const ADD_IMAGEBLOCK_SHORTCUT = "meta+i";
export const ADD_FEEDBLOCK_SHORTCUT = "meta+f";