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 (
<div className="flex flex-col">
<NewMessageModal />
{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>
)}
<NewMessageModal />
</div>
);
}

View File

@ -10,7 +10,7 @@ import { useNavigate } from "react-router-dom";
export function NewMessageModal() {
const navigate = useNavigate();
const { status, data, isFetching }: any = useQuery(["plebs"], async () => {
const { status, data }: any = useQuery(["plebs"], async () => {
return await getPlebs();
});
@ -96,7 +96,7 @@ export function NewMessageModal() {
</div>
</div>
<div className="h-[500px] flex flex-col pb-5 overflow-x-hidden overflow-y-auto">
{status === "loading" || isFetching ? (
{status === "loading" ? (
<p>Loading...</p>
) : (
data.map((pleb) => (
@ -111,10 +111,10 @@ export function NewMessageModal() {
className="w-9 h-9 shrink-0 object-cover rounded"
/>
<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}
</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.npub.substring(0, 16).concat("...")}
</span>

View File

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

View File

@ -1,66 +1,11 @@
import { AddFeedBlock } from "@app/space/components/addFeed";
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() {
const [imageModal, setImageModal] = useState(false);
const [feedModal, setFeedModal] = useState(false);
const openAddImageModal = () => {
setImageModal(true);
};
const openAddFeedModal = () => {
setFeedModal(true);
};
return (
<>
<Menu as="div" className="relative inline-block text-left">
<Menu.Button className="group inline-flex flex-col items-center gap-2.5">
<div className="inline-flex h-9 w-9 shrink items-center justify-center rounded-lg bg-zinc-900 group-hover:bg-zinc-800">
<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} />}
</>
<div className="flex flex-col gap-1">
<AddImageBlock />
<AddFeedBlock />
</div>
);
}

View File

@ -1,24 +1,37 @@
import { User } from "@app/auth/components/user";
import { Dialog, Transition } from "@headlessui/react";
import { createBlock } from "@libs/storage";
import { CancelIcon } from "@shared/icons";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Combobox } from "@headlessui/react";
import { createBlock, getPlebs } from "@libs/storage";
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 { Fragment, useState } from "react";
import { Fragment, useEffect, useState } from "react";
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 [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 = () => {
// update local state
setIsOpen(false);
// update parent state
parentState(false);
};
useHotkeys(ADD_FEEDBLOCK_SHORTCUT, () => openModal());
const block = useMutation({
mutationFn: (data: any) => {
return createBlock(data.kind, data.title, data.content);
@ -38,151 +51,224 @@ export function AddFeedBlock({ parentState }: { parentState: any }) {
const onSubmit = (data: any) => {
setLoading(true);
let pubkey = data.content;
if (pubkey.substring(0, 4) === "npub") {
pubkey = nip19.decode(pubkey).data;
}
selected.forEach((item, index) => {
if (item.substring(0, 4) === "npub") {
selected[index] = nip19.decode(item).data;
}
});
// 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);
// reset form
reset();
// close modal
closeModal();
}, 1200);
setLoading(false);
// reset form
reset();
// close modal
closeModal();
};
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
</Transition.Child>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<>
<button
type="button"
onClick={() => openModal()}
className="inline-flex w-56 h-9 items-center justify-start gap-2.5 rounded-md px-2.5"
>
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<CommandIcon width={12} height={12} className="text-zinc-500" />
</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">
<span className="text-zinc-500 text-sm leading-none">F</span>
</div>
</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
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
leaveFrom="opacity-100"
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="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Dialog.Title
as="h3"
className="text-lg font-semibold leading-none text-zinc-100"
>
Create image 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 className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
</Transition.Child>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<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="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Dialog.Title
as="h3"
className="text-lg font-semibold leading-none text-zinc-100"
>
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>
<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 h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col gap-4 mb-0"
>
<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">
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col gap-4 mb-0"
>
<div className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
Title *
</label>
<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"
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 className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
Pubkey OR Npub *
</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("content", {
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 className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
Choose at least 1 user *
</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="w-full px-3 py-2">
<Combobox
value={selected}
onChange={setSelected}
multiple
>
<Combobox.Input
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>
<button
type="submit"
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"
>
{loading ? (
<svg
className="h-4 w-4 animate-spin text-black dark:text-zinc-100"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
cy="12"
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>
<div>
<button
type="submit"
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"
>
{loading ? (
<svg
className="h-4 w-4 animate-spin text-black dark:text-zinc-100"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
cy="12"
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,10 +1,11 @@
import { Dialog, Transition } from "@headlessui/react";
import { createBlock } from "@libs/storage";
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 { RelayContext } from "@shared/relayProvider";
import { DEFAULT_AVATAR } from "@stores/constants";
import { ADD_IMAGEBLOCK_SHORTCUT } from "@stores/shortcuts";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { open } from "@tauri-apps/api/dialog";
import { Body, fetch } from "@tauri-apps/api/http";
@ -13,26 +14,30 @@ import { dateToUnix } from "@utils/date";
import { useAccount } from "@utils/hooks/useAccount";
import { Fragment, useContext, useEffect, useRef, useState } from "react";
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 queryClient = useQueryClient();
const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(true);
const [isOpen, setIsOpen] = useState(false);
const [image, setImage] = useState("");
const { account } = useAccount();
const tags = useRef(null);
const closeModal = () => {
// update local state
setIsOpen(false);
// update parent state
parentState(false);
const openModal = () => {
setIsOpen(true);
};
const closeModal = () => {
setIsOpen(false);
};
useHotkeys(ADD_IMAGEBLOCK_SHORTCUT, () => openModal());
const {
register,
handleSubmit,
@ -118,13 +123,11 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
// mutate
block.mutate({ kind: 0, title: data.title, content: data.content });
setTimeout(() => {
setLoading(false);
// reset form
reset();
// close modal
closeModal();
}, 1200);
setLoading(false);
// reset form
reset();
// close modal
closeModal();
};
useEffect(() => {
@ -132,145 +135,164 @@ export function AddImageBlock({ parentState }: { parentState: any }) {
}, [setValue, image]);
return (
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-50" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
</Transition.Child>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<>
<button
type="button"
onClick={() => openModal()}
className="inline-flex w-56 h-9 items-center justify-start gap-2.5 rounded-md px-2.5"
>
<div className="flex items-center gap-2">
<div className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border-t border-zinc-800/50 bg-zinc-900">
<CommandIcon width={12} height={12} className="text-zinc-500" />
</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">
<span className="text-zinc-500 text-sm leading-none">I</span>
</div>
</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
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
leaveFrom="opacity-100"
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="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Dialog.Title
as="h3"
className="text-lg font-semibold leading-none text-zinc-100"
>
Create image 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">
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 h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col gap-4 mb-0"
>
<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 className="fixed inset-0 z-50 bg-black bg-opacity-30 backdrop-blur-md" />
</Transition.Child>
<div className="fixed inset-0 z-50 flex min-h-full items-center justify-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<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="h-min w-full shrink-0 border-b border-zinc-800 px-5 py-5">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<Dialog.Title
as="h3"
className="text-lg font-semibold leading-none text-zinc-100"
>
Create image 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">
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 className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
Picture
</label>
<div className="relative inline-flex h-56 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
<Image
src={image}
fallback={DEFAULT_AVATAR}
alt="content"
className="relative z-10 max-h-[156px] h-auto w-[150px] object-cover rounded-md"
/>
<div className="absolute bottom-3 right-3 z-10">
<button
onClick={() => openFileDialog()}
type="button"
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"
>
Upload
</button>
</div>
<div className="flex h-full w-full flex-col overflow-y-auto px-5 pb-5 pt-3">
<form
onSubmit={handleSubmit(onSubmit)}
className="flex h-full w-full flex-col gap-4 mb-0"
>
<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>
</div>
<div>
<button
type="submit"
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"
>
{loading ? (
<svg
className="h-4 w-4 animate-spin text-black dark:text-zinc-100"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
cy="12"
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>
<div className="flex flex-col gap-1">
<label className="text-sm font-medium uppercase tracking-wider text-zinc-400">
Picture
</label>
<div className="relative inline-flex h-56 w-full items-center justify-center overflow-hidden rounded-lg border border-zinc-900 bg-zinc-950">
<Image
src={image}
fallback={DEFAULT_AVATAR}
alt="content"
className="relative z-10 max-h-[156px] h-auto w-[150px] object-cover rounded-md"
/>
<div className="absolute bottom-3 right-3 z-10">
<button
onClick={() => openFileDialog()}
type="button"
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"
>
Upload
</button>
</div>
</div>
</div>
<div>
<button
type="submit"
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"
>
{loading ? (
<svg
className="h-4 w-4 animate-spin text-black dark:text-zinc-100"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<title id="loading">Loading</title>
<circle
className="opacity-25"
cx="12"
cy="12"
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 { NoteSkeleton } from "@shared/notes/skeleton";
import { TitleBar } from "@shared/titleBar";
@ -25,7 +25,7 @@ export function FeedBlock({ params }: { params: any }) {
}: any = useInfiniteQuery({
queryKey: ["newsfeed", params.content],
queryFn: async ({ pageParam = 0 }) => {
return await getNotesByAuthor(
return await getNotesByAuthors(
params.content,
TIME,
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"
style={{ contain: "strict" }}
>
{status === "loading" || isFetching ? (
{status === "loading" ? (
<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 />
</div>
</div>
@ -119,6 +119,13 @@ export function FeedBlock({ params }: { params: any }) {
</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>
);

View File

@ -112,7 +112,7 @@ export function FollowingBlock({ block }: { block: number }) {
>
{status === "loading" ? (
<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 />
</div>
</div>
@ -140,7 +140,7 @@ export function FollowingBlock({ block }: { block: number }) {
)}
{isFetching && !isFetchingNextPage && (
<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 />
</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="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="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
type="button"
onClick={() => block.mutate(params.id)}

View File

@ -65,12 +65,12 @@ export function SpaceScreen() {
</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">
<AddBlock />
</div>
</div>
<div className="shrink-0 w-[360px]" />
<div className="shrink-0 w-[350px]" />
</div>
);
}

View File

@ -135,12 +135,14 @@ export async function countTotalNotes() {
const result = await db.select(
'SELECT COUNT(*) AS "total" FROM notes WHERE kind IN (1, 6);',
);
return result[0].total;
return parseInt(result[0].total);
}
// get all notes
export async function getNotes(time: number, limit: number, offset: number) {
const db = await connect();
const totalNotes = await countTotalNotes();
const nextCursor = offset + limit;
const notes: any = { data: null, nextCursor: 0 };
const query: any = await db.select(
@ -148,19 +150,22 @@ export async function getNotes(time: number, limit: number, offset: number) {
);
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 getNotesByAuthor(
// get all notes by pubkey
export async function getNotesByPubkey(
pubkey: string,
time: number,
limit: number,
offset: number,
) {
const db = await connect();
const totalNotes = await countTotalNotes();
const nextCursor = offset + limit;
const notes: any = { data: null, nextCursor: 0 };
const query: any = await db.select(
@ -168,7 +173,33 @@ export async function getNotesByAuthor(
);
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;
}

View File

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

View File

@ -53,7 +53,7 @@ export function Note({ event, block }: Note) {
Lume isn't fully support this kind in newsfeed
</p>
</div>
<div className="markdown">
<div className="select-text whitespace-pre-line break-words text-base text-zinc-100">
<p>{event.content}</p>
</div>
</div>
@ -63,7 +63,7 @@ export function Note({ event, block }: Note) {
return (
<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}
<div className="flex flex-col">
<User

View File

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

View File

@ -10,7 +10,7 @@ export function ImagePreview({ urls }: { urls: string[] }) {
src={url}
fallback="https://void.cat/d/XTmrMkpid8DGLjv1AzdvcW"
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>
))}

View File

@ -20,7 +20,7 @@ export function LinkPreview({ urls }: { urls: string[] }) {
</div>
) : (
<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]}
target="_blank"
rel="noreferrer"

View File

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

View File

@ -29,8 +29,10 @@ export function RepliesList({ parent_id }: { parent_id: string }) {
<div className="px=3">
<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">
<EmptyIcon width={56} height={56} />
<p className="text-zinc-500 text-sm font-medium">No replies</p>
<h3 className="text-3xl">👋</h3>
<p className="leading-none text-zinc-400">
Share your thought on it...
</p>
</div>
</div>
</div>

View File

@ -15,7 +15,8 @@ export function Repost({
const { status, data, isFetching } = useEvent(repostID);
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" ? (
<NoteSkeleton />
) : (
@ -34,7 +35,7 @@ export function Repost({
Lume isn't fully support this kind in newsfeed
</p>
</div>
<div className="markdown">
<div className="select-text whitespace-pre-line break-words text-base text-zinc-100">
<p>{data.content || data.toString()}</p>
</div>
</div>

View File

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

View File

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