add edit profile
This commit is contained in:
@@ -2,7 +2,7 @@ import { Dialog, Transition } from "@headlessui/react";
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { CancelIcon, HideIcon } from "@shared/icons";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { Tooltip } from "@shared/tooltip";
|
||||
import { Tooltip } from "@shared/tooltip_dep";
|
||||
import { useChannelMessages } from "@stores/channels";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Dialog, Transition } from "@headlessui/react";
|
||||
import { NDKEvent, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
|
||||
import { CancelIcon, MuteIcon } from "@shared/icons";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { Tooltip } from "@shared/tooltip";
|
||||
import { Tooltip } from "@shared/tooltip_dep";
|
||||
import { useChannelMessages } from "@stores/channels";
|
||||
import { dateToUnix } from "@utils/date";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReplyMessageIcon } from "@shared/icons";
|
||||
import { Tooltip } from "@shared/tooltip";
|
||||
import { Tooltip } from "@shared/tooltip_dep";
|
||||
import { useChannelMessages } from "@stores/channels";
|
||||
|
||||
export function MessageReplyButton({
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { UserFeed } from "@app/user/components/feed";
|
||||
import { UserMetadata } from "@app/user/components/metadata";
|
||||
import { Tab } from "@headlessui/react";
|
||||
import { EditProfileModal } from "@shared/editProfileModal";
|
||||
import { ThreadsIcon, ZapIcon } from "@shared/icons";
|
||||
import { Image } from "@shared/image";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
import { useProfile } from "@utils/hooks/useProfile";
|
||||
import { useSocial } from "@utils/hooks/useSocial";
|
||||
import { shortenKey } from "@utils/shortenKey";
|
||||
@@ -13,6 +15,7 @@ import { Link, useParams } from "react-router-dom";
|
||||
export function UserScreen() {
|
||||
const { pubkey } = useParams();
|
||||
const { user } = useProfile(pubkey);
|
||||
const { account } = useAccount();
|
||||
const { status, userFollows, follow, unfollow } = useSocial();
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
@@ -116,6 +119,8 @@ export function UserScreen() {
|
||||
>
|
||||
<ZapIcon className="w-5 h-5" />
|
||||
</button>
|
||||
<span className="inline-flex mx-2 w-px h-4 bg-zinc-900" />
|
||||
{account && account.pubkey === pubkey && <EditProfileModal />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useProfile } from "@utils/hooks/useProfile";
|
||||
import { sendNativeNotification } from "@utils/notification";
|
||||
import { produce } from "immer";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const lastLogin = await getLastLogin();
|
||||
|
||||
@@ -92,7 +93,10 @@ export function ActiveAccount({ data }: { data: any }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative inline-block h-9 w-9">
|
||||
<Link
|
||||
to={`/app/user/${data.pubkey}`}
|
||||
className="relative inline-block h-9 w-9"
|
||||
>
|
||||
<Image
|
||||
src={user.image}
|
||||
fallback={DEFAULT_AVATAR}
|
||||
@@ -100,6 +104,6 @@ export function ActiveAccount({ data }: { data: any }) {
|
||||
className="h-9 w-9 rounded-md object-cover"
|
||||
/>
|
||||
<NetworkStatusIndicator />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { createBlobFromFile } from "@utils/createBlobFromFile";
|
||||
|
||||
import { LoaderIcon } from "./icons";
|
||||
import { LoaderIcon, PlusIcon } from "@shared/icons";
|
||||
import { open } from "@tauri-apps/api/dialog";
|
||||
import { Body, fetch } from "@tauri-apps/api/http";
|
||||
import { createBlobFromFile } from "@utils/createBlobFromFile";
|
||||
import { useState } from "react";
|
||||
|
||||
export function AvatarUploader({ valueState }: { valueState: any }) {
|
||||
export function AvatarUploader({ setPicture }: { setPicture: any }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const openFileDialog = async () => {
|
||||
@@ -44,23 +43,26 @@ export function AvatarUploader({ valueState }: { valueState: any }) {
|
||||
body: Body.bytes(buf),
|
||||
},
|
||||
);
|
||||
const webpImage = `https://void.cat/d/${res.data.file.id}.webp`;
|
||||
const image = `https://void.cat/d/${res.data.file.id}.jpg`;
|
||||
|
||||
valueState(webpImage);
|
||||
// update parent state
|
||||
setPicture(image);
|
||||
|
||||
// disable loader
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => openFileDialog()}
|
||||
type="button"
|
||||
className="inline-flex h-7 items-center justify-center rounded bg-zinc-900 px-3 text-sm font-medium text-zinc-200 hover:bg-zinc-800"
|
||||
onClick={() => openFileDialog()}
|
||||
className="w-full h-full inline-flex items-center justify-center bg-zinc-900/40"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
<LoaderIcon className="h-6 w-6 animate-spintext-zinc-100" />
|
||||
) : (
|
||||
<span className="leading-none">Upload</span>
|
||||
<PlusIcon className="h-6 w-6 text-zinc-100" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
|
||||
69
src/shared/bannerUploader.tsx
Normal file
69
src/shared/bannerUploader.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { LoaderIcon, PlusIcon } from "@shared/icons";
|
||||
import { open } from "@tauri-apps/api/dialog";
|
||||
import { Body, fetch } from "@tauri-apps/api/http";
|
||||
import { createBlobFromFile } from "@utils/createBlobFromFile";
|
||||
import { useState } from "react";
|
||||
|
||||
export function BannerUploader({ setBanner }: { setBanner: any }) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const openFileDialog = async () => {
|
||||
const selected: any = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: "Image",
|
||||
extensions: ["png", "jpeg", "jpg", "gif"],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (Array.isArray(selected)) {
|
||||
// user selected multiple files
|
||||
} else if (selected === null) {
|
||||
// user cancelled the selection
|
||||
} else {
|
||||
setLoading(true);
|
||||
|
||||
const filename = selected.split("/").pop();
|
||||
const file = await createBlobFromFile(selected);
|
||||
const buf = await file.arrayBuffer();
|
||||
|
||||
const res: { data: { file: { id: string } } } = await fetch(
|
||||
"https://void.cat/upload?cli=false",
|
||||
{
|
||||
method: "POST",
|
||||
timeout: 5,
|
||||
headers: {
|
||||
accept: "*/*",
|
||||
"Content-Type": "application/octet-stream",
|
||||
"V-Filename": filename,
|
||||
"V-Description": "Upload from https://lume.nu",
|
||||
"V-Strip-Metadata": "true",
|
||||
},
|
||||
body: Body.bytes(buf),
|
||||
},
|
||||
);
|
||||
const image = `https://void.cat/d/${res.data.file.id}.jpg`;
|
||||
|
||||
// update parent state
|
||||
setBanner(image);
|
||||
|
||||
// disable loader
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openFileDialog()}
|
||||
className="w-full h-full inline-flex items-center justify-center bg-zinc-900/40"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-8 w-8 animate-spintext-zinc-100" />
|
||||
) : (
|
||||
<PlusIcon className="h-8 w-8 text-zinc-100" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
226
src/shared/editProfileModal.tsx
Normal file
226
src/shared/editProfileModal.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { usePublish } from "@libs/ndk";
|
||||
import { getPleb } from "@libs/storage";
|
||||
import { AvatarUploader } from "@shared/avatarUploader";
|
||||
import { BannerUploader } from "@shared/bannerUploader";
|
||||
import { CancelIcon, LoaderIcon } from "@shared/icons";
|
||||
import { Image } from "@shared/image";
|
||||
import { DEFAULT_AVATAR } from "@stores/constants";
|
||||
import { useAccount } from "@utils/hooks/useAccount";
|
||||
import { Fragment, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
export function EditProfileModal() {
|
||||
const publish = usePublish();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [picture, setPicture] = useState(DEFAULT_AVATAR);
|
||||
const [banner, setBanner] = useState(null);
|
||||
|
||||
const { account } = useAccount();
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isValid },
|
||||
} = useForm({
|
||||
defaultValues: async () => {
|
||||
const res = await getPleb(account.npub);
|
||||
if (res.picture) {
|
||||
setPicture(res.image);
|
||||
}
|
||||
if (res.banner) {
|
||||
setBanner(res.banner);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const onSubmit = (data: any) => {
|
||||
// start loading
|
||||
setLoading(true);
|
||||
|
||||
// publish
|
||||
const event = publish({
|
||||
content: JSON.stringify({
|
||||
...data,
|
||||
display_name: data.name,
|
||||
bio: data.about,
|
||||
image: data.picture,
|
||||
}),
|
||||
kind: 0,
|
||||
tags: [],
|
||||
});
|
||||
|
||||
if (event) {
|
||||
setTimeout(() => {
|
||||
// reset form
|
||||
reset();
|
||||
// reset state
|
||||
setLoading(false);
|
||||
setIsOpen(false);
|
||||
setPicture(DEFAULT_AVATAR);
|
||||
setBanner(null);
|
||||
}, 1200);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openModal()}
|
||||
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
|
||||
>
|
||||
Edit profile
|
||||
</button>
|
||||
<Transition appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" 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">
|
||||
<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 rounded-lg 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 items-center justify-between">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-semibold leading-none text-zinc-100"
|
||||
>
|
||||
Edit profile
|
||||
</Dialog.Title>
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="inline-flex h-5 w-5 items-center justify-center rounded hover:bg-zinc-900"
|
||||
>
|
||||
<CancelIcon className="w-5 h-5 text-zinc-300" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-full flex-col overflow-y-auto">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mb-0">
|
||||
<input
|
||||
type={"hidden"}
|
||||
{...register("picture")}
|
||||
value={picture}
|
||||
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"
|
||||
/>
|
||||
<input
|
||||
type={"hidden"}
|
||||
{...register("banner")}
|
||||
value={banner}
|
||||
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="relative">
|
||||
<div className="relative w-full h-44 bg-zinc-800">
|
||||
<Image
|
||||
src={banner}
|
||||
fallback="https://void.cat/d/QY1myro5tkHVs2nY7dy74b.jpg"
|
||||
alt="user's banner"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10 w-full h-full">
|
||||
<BannerUploader setBanner={setBanner} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 mb-5">
|
||||
<div className="z-10 relative h-14 w-14 -mt-7">
|
||||
<Image
|
||||
src={picture}
|
||||
fallback={DEFAULT_AVATAR}
|
||||
alt="user's avatar"
|
||||
className="h-14 w-14 object-cover ring-2 ring-zinc-900 rounded-lg"
|
||||
/>
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-10 w-full h-full">
|
||||
<AvatarUploader setPicture={setPicture} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 px-4 pb-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
{...register("name", {
|
||||
required: true,
|
||||
minLength: 4,
|
||||
})}
|
||||
spellCheck={false}
|
||||
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Bio
|
||||
</label>
|
||||
<textarea
|
||||
{...register("about")}
|
||||
spellCheck={false}
|
||||
className="relative resize-none h-20 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<label className="text-sm font-semibold uppercase tracking-wider text-zinc-400">
|
||||
Website
|
||||
</label>
|
||||
<input
|
||||
type={"text"}
|
||||
{...register("website", { required: false })}
|
||||
spellCheck={false}
|
||||
className="relative h-10 w-full rounded-lg px-3 py-2 !outline-none bg-zinc-800 text-zinc-100 placeholder:text-zinc-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!isValid}
|
||||
className="inline-flex items-center justify-center gap-1 transform active:translate-y-1 disabled:pointer-events-none disabled:opacity-50 focus:outline-none h-11 w-full bg-fuchsia-500 rounded-md font-medium text-zinc-100 hover:bg-fuchsia-600"
|
||||
>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
"Update"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -13,8 +13,11 @@ export function ThreadIcon(
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7.5 5.75a1.75 1.75 0 113.5 0 1.75 1.75 0 01-3.5 0zM13 5.75a1.75 1.75 0 113.5 0 1.75 1.75 0 01-3.5 0zM7.5 18.25a1.75 1.75 0 113.5 0 1.75 1.75 0 01-3.5 0zM13 18.25a1.75 1.75 0 113.5 0 1.75 1.75 0 01-3.5 0zM7.5 11.9a1.75 1.75 0 113.5 0v.1a1.75 1.75 0 11-3.5 0v-.1zM13 11.9a1.75 1.75 0 113.5 0v.1a1.75 1.75 0 11-3.5 0v-.1z"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="1.5"
|
||||
d="M2.75 12h18.5M2.75 5.75h18.5m-18.5 12.5h8.75"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MediaIcon } from "@shared/icons";
|
||||
import { Tooltip } from "@shared/tooltip";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { LoaderIcon, MediaIcon } from "@shared/icons";
|
||||
import { open } from "@tauri-apps/api/dialog";
|
||||
import { Body, fetch } from "@tauri-apps/api/http";
|
||||
import { createBlobFromFile } from "@utils/createBlobFromFile";
|
||||
@@ -56,42 +56,35 @@ export function MediaUploader({ setState }: { setState: any }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip message="Upload media">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openFileDialog()}
|
||||
className="group inline-flex h-6 w-6 items-center justify-center rounded bg-zinc-700 hover:bg-zinc-600"
|
||||
>
|
||||
{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"
|
||||
<Tooltip.Provider>
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openFileDialog()}
|
||||
className="group inline-flex h-6 w-6 items-center justify-center rounded bg-zinc-700 hover:bg-zinc-600"
|
||||
>
|
||||
<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>
|
||||
) : (
|
||||
<MediaIcon
|
||||
width={14}
|
||||
height={14}
|
||||
className="text-zinc-400 group-hover:text-zinc-200"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</Tooltip>
|
||||
{loading ? (
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-black dark:text-zinc-100" />
|
||||
) : (
|
||||
<MediaIcon
|
||||
width={14}
|
||||
height={14}
|
||||
className="text-zinc-400 group-hover:text-zinc-200"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="-left-10 data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade select-none text-sm rounded-md text-zinc-100 bg-zinc-800/80 backdrop-blur-lg px-3.5 py-1.5 leading-none will-change-[transform,opacity]"
|
||||
sideOffset={5}
|
||||
>
|
||||
Upload media
|
||||
<Tooltip.Arrow className="fill-zinc-800/80 backdrop-blur-lg" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { createReplyNote } from "@libs/storage";
|
||||
import { createBlock, createReplyNote } from "@libs/storage";
|
||||
import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk";
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { LoaderIcon, ReplyIcon, RepostIcon, ZapIcon } from "@shared/icons";
|
||||
import { ThreadIcon } from "@shared/icons/thread";
|
||||
import { NoteReply } from "@shared/notes/metadata/reply";
|
||||
import { NoteRepost } from "@shared/notes/metadata/repost";
|
||||
import { NoteZap } from "@shared/notes/metadata/zap";
|
||||
import { RelayContext } from "@shared/relayProvider";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { decode } from "light-bolt11-decoder";
|
||||
import { useContext } from "react";
|
||||
|
||||
@@ -19,6 +21,8 @@ export function NoteMetadata({
|
||||
eventPubkey: string;
|
||||
}) {
|
||||
const ndk = useContext(RelayContext);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { status, data } = useQuery(["note-metadata", id], async () => {
|
||||
let replies = 0;
|
||||
let reposts = 0;
|
||||
@@ -67,49 +71,71 @@ export function NoteMetadata({
|
||||
return { replies, reposts, zap };
|
||||
});
|
||||
|
||||
const block = useMutation({
|
||||
mutationFn: (data: any) => {
|
||||
return createBlock(data.kind, data.title, data.content);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["blocks"] });
|
||||
},
|
||||
});
|
||||
|
||||
const openThread = (thread: string) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection.toString().length === 0) {
|
||||
block.mutate({ kind: 2, title: "Thread", content: thread });
|
||||
} else {
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
if (status === "loading") {
|
||||
return (
|
||||
<div className="inline-flex items-center w-full h-12 mt-2">
|
||||
<div className="w-20 group inline-flex items-center gap-1.5">
|
||||
<ReplyIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<LoaderIcon
|
||||
width={12}
|
||||
height={12}
|
||||
className="animate-spin text-black dark:text-zinc-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-20 group inline-flex items-center gap-1.5">
|
||||
<RepostIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<LoaderIcon
|
||||
width={12}
|
||||
height={12}
|
||||
className="animate-spin text-black dark:text-zinc-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-20 group inline-flex items-center gap-1.5">
|
||||
<ZapIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<LoaderIcon
|
||||
width={12}
|
||||
height={12}
|
||||
className="animate-spin text-black dark:text-zinc-100"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="inline-flex items-center w-full h-12 mt-2">
|
||||
{status === "loading" ? (
|
||||
<>
|
||||
<div className="w-20 group inline-flex items-center gap-1.5">
|
||||
<ReplyIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<LoaderIcon
|
||||
width={12}
|
||||
height={12}
|
||||
className="animate-spin text-black dark:text-zinc-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-20 group inline-flex items-center gap-1.5">
|
||||
<RepostIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<LoaderIcon
|
||||
width={12}
|
||||
height={12}
|
||||
className="animate-spin text-black dark:text-zinc-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-20 group inline-flex items-center gap-1.5">
|
||||
<ZapIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<LoaderIcon
|
||||
width={12}
|
||||
height={12}
|
||||
className="animate-spin text-black dark:text-zinc-100"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Tooltip.Provider>
|
||||
<div className="inline-flex items-center justify-between w-full h-12 mt-2">
|
||||
<div className="inline-flex justify-between items-center">
|
||||
<NoteReply
|
||||
id={id}
|
||||
rootID={rootID}
|
||||
@@ -118,8 +144,28 @@ export function NoteMetadata({
|
||||
/>
|
||||
<NoteRepost id={id} pubkey={eventPubkey} reposts={data.reposts} />
|
||||
<NoteZap zaps={data.zap} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openThread(id)}
|
||||
className="w-6 h-6 inline-flex items-center justify-center rounded border-t border-zinc-700/50 bg-zinc-800 hover:bg-zinc-700"
|
||||
>
|
||||
<ThreadIcon className="w-4 h-4 text-zinc-400" />
|
||||
</button>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="-left-10 data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade select-none text-sm rounded-md text-zinc-100 bg-zinc-800/80 backdrop-blur-lg px-3.5 py-1.5 leading-none will-change-[transform,opacity]"
|
||||
sideOffset={5}
|
||||
>
|
||||
Open thread
|
||||
<Tooltip.Arrow className="fill-zinc-800/80 backdrop-blur-lg" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</div>
|
||||
</Tooltip.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { ReplyIcon } from "@shared/icons";
|
||||
import { useComposer } from "@stores/composer";
|
||||
import { compactNumber } from "@utils/number";
|
||||
@@ -11,19 +12,30 @@ export function NoteReply({
|
||||
const setReply = useComposer((state) => state.setReply);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReply(id, rootID, pubkey)}
|
||||
className="w-20 group inline-flex items-center gap-1.5"
|
||||
>
|
||||
<ReplyIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-green-400"
|
||||
/>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-zinc-100">
|
||||
{compactNumber.format(replies)}
|
||||
</span>
|
||||
</button>
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setReply(id, rootID, pubkey)}
|
||||
className="group w-20 h-6 group inline-flex items-center gap-1.5"
|
||||
>
|
||||
<Tooltip.Trigger asChild>
|
||||
<span className="inline-flex items-center justify-center w-6 h-6 rounded group-hover:bg-zinc-800">
|
||||
<ReplyIcon className="w-4 h-4 text-zinc-400 group-hover:text-green-500" />
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-zinc-100">
|
||||
{compactNumber.format(replies)}
|
||||
</span>
|
||||
</button>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="-left-10 data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade select-none text-sm rounded-md text-zinc-100 bg-zinc-800/80 backdrop-blur-lg px-3.5 py-1.5 leading-none will-change-[transform,opacity]"
|
||||
sideOffset={5}
|
||||
>
|
||||
Quick reply
|
||||
<Tooltip.Arrow className="fill-zinc-800/80 backdrop-blur-lg" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { RepostIcon } from "@shared/icons";
|
||||
import { useComposer } from "@stores/composer";
|
||||
import { compactNumber } from "@utils/number";
|
||||
@@ -10,19 +11,30 @@ export function NoteRepost({
|
||||
const setRepost = useComposer((state) => state.setRepost);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRepost(id, pubkey)}
|
||||
className="w-20 group inline-flex items-center gap-1.5"
|
||||
>
|
||||
<RepostIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-blue-400"
|
||||
/>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-zinc-100">
|
||||
{compactNumber.format(reposts)}
|
||||
</span>
|
||||
</button>
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRepost(id, pubkey)}
|
||||
className="group w-20 h-6 group inline-flex items-center gap-1.5"
|
||||
>
|
||||
<Tooltip.Trigger asChild>
|
||||
<span className="inline-flex items-center justify-center w-6 h-6 rounded group-hover:bg-zinc-800">
|
||||
<RepostIcon className="w-4 h-4 text-zinc-400 group-hover:text-blue-400" />
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-zinc-100">
|
||||
{compactNumber.format(reposts)}
|
||||
</span>
|
||||
</button>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="-left-10 data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade select-none text-sm rounded-md text-zinc-100 bg-zinc-800/80 backdrop-blur-lg px-3.5 py-1.5 leading-none will-change-[transform,opacity]"
|
||||
sideOffset={5}
|
||||
>
|
||||
Repost
|
||||
<Tooltip.Arrow className="fill-zinc-800/80 backdrop-blur-lg" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,32 @@
|
||||
import * as Tooltip from "@radix-ui/react-tooltip";
|
||||
import { ZapIcon } from "@shared/icons";
|
||||
import { compactNumber } from "@utils/number";
|
||||
|
||||
export function NoteZap({ zaps }: { zaps: number }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="w-20 group inline-flex items-center gap-1.5"
|
||||
>
|
||||
<ZapIcon
|
||||
width={16}
|
||||
height={16}
|
||||
className="text-zinc-400 group-hover:text-blue-400"
|
||||
/>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-zinc-100">
|
||||
{compactNumber.format(zaps)}
|
||||
</span>
|
||||
</button>
|
||||
<Tooltip.Root delayDuration={150}>
|
||||
<button
|
||||
type="button"
|
||||
className="group w-20 h-6 group inline-flex items-center gap-1.5"
|
||||
>
|
||||
<Tooltip.Trigger asChild>
|
||||
<span className="inline-flex items-center justify-center w-6 h-6 rounded group-hover:bg-zinc-800">
|
||||
<ZapIcon className="w-4 h-4 text-zinc-400 group-hover:text-orange-400" />
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<span className="text-base leading-none text-zinc-400 group-hover:text-zinc-100">
|
||||
{compactNumber.format(zaps)}
|
||||
</span>
|
||||
</button>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
className="-left-10 data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade data-[state=delayed-open]:data-[side=bottom]:animate-slideUpAndFade select-none text-sm rounded-md text-zinc-100 bg-zinc-800/80 backdrop-blur-lg px-3.5 py-1.5 leading-none will-change-[transform,opacity]"
|
||||
sideOffset={5}
|
||||
>
|
||||
Coming Soon
|
||||
<Tooltip.Arrow className="fill-zinc-800/80 backdrop-blur-lg" />
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,12 +24,10 @@ export function useProfile(id: string) {
|
||||
const result = await getPleb(npub);
|
||||
|
||||
if (result && parseInt(result.created_at) + 86400 >= current) {
|
||||
console.log("cache", result);
|
||||
return result;
|
||||
} else {
|
||||
const user = ndk.getUser({ npub });
|
||||
await user.fetchProfile();
|
||||
console.log("new", user);
|
||||
await createPleb(id, user.profile);
|
||||
|
||||
return user.profile;
|
||||
|
||||
Reference in New Issue
Block a user