add user page

This commit is contained in:
Ren Amamiya
2023-06-28 17:23:36 +07:00
parent 3fe601cfc6
commit ec1ff9ab87
27 changed files with 492 additions and 491 deletions

View File

@@ -13,7 +13,7 @@ export function ChatsListItem({ data }: { data: any }) {
<div className="inline-flex h-9 items-center gap-2.5 rounded-md px-2.5">
<div className="relative h-6 w-6 shrink-0 animate-pulse rounded bg-zinc-800" />
<div>
<div className="h-2.5 w-full animate-pulse truncate rounded bg-zinc-800 text-base font-medium" />
<div className="h-2.5 w-2/3 animate-pulse truncate rounded bg-zinc-800 text-base font-medium" />
</div>
</div>
);
@@ -41,7 +41,10 @@ export function ChatsListItem({ data }: { data: any }) {
<div className="w-full inline-flex items-center justify-between">
<div className="inline-flex items-baseline gap-1">
<h5 className="max-w-[9rem] truncate font-medium text-zinc-200">
{user?.nip05 || user?.displayName || shortenKey(data.sender_pubkey)}
{user?.nip05 ||
user?.displayName ||
user?.name ||
shortenKey(data.sender_pubkey)}
</h5>
</div>
<div className="flex items-center">

View File

@@ -112,7 +112,7 @@ export function NewMessageModal() {
/>
<div className="inline-flex flex-col gap-1">
<h3 className="leading-none max-w-[15rem] line-clamp-1 font-medium text-zinc-100">
{pleb.display_name || pleb.name}
{pleb.displayName || pleb.name}
</h3>
<span className="leading-none max-w-[10rem] line-clamp-1 text-sm text-zinc-400">
{pleb.nip05 ||

View File

@@ -83,7 +83,7 @@ export function FollowingBlock({ block }: { block: number }) {
return (
<div className="shrink-0 relative w-[400px] border-r border-zinc-900">
<TitleBar title="Circle" />
<TitleBar title="Your Circle" />
{hasNewNote && (
<div className="z-50 absolute top-12 left-1/2 transform -translate-x-1/2">
<button

View File

@@ -0,0 +1,20 @@
import { getNotesByPubkey } from "@libs/storage";
import { Note } from "@shared/notes/note";
import { useQuery } from "@tanstack/react-query";
import { LumeEvent } from "@utils/types";
export function UserFeed({ pubkey }: { pubkey: string }) {
const { status, data } = useQuery(["user-feed", pubkey], async () => {
return await getNotesByPubkey(pubkey);
});
return (
<div className="w-full max-w-[400px] px-2">
{status === "loading" ? (
<p>Loading...</p>
) : (
data.map((note: LumeEvent) => <Note key={note.event_id} event={note} />)
)}
</div>
);
}

View File

@@ -0,0 +1,55 @@
import { useQuery } from "@tanstack/react-query";
import { compactNumber } from "@utils/number";
export function UserMetadata({ pubkey }: { pubkey: string }) {
const { status, data } = useQuery(["user-metadata", pubkey], async () => {
const res = await fetch(
`https://api.nostr.band/v0/stats/profile/${pubkey}`,
);
if (!res.ok) {
throw new Error("Error");
}
return await res.json();
});
if (status === "loading") {
return <p>Loading...</p>;
}
return (
<div className="w-full flex items-center gap-10">
<div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100">
{data.stats[pubkey].followers_pubkey_count ?? 0}
</span>
<span className="leading-none text-sm text-zinc-400">Followers</span>
</div>
<div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100">
{data.stats[pubkey].pub_following_pubkey_count ?? 0}
</span>
<span className="leading-none text-sm text-zinc-400">Following</span>
</div>
<div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100">
{data.stats[pubkey].zaps_received
? compactNumber.format(
data.stats[pubkey].zaps_received.msats / 1000,
)
: 0}
</span>
<span className="leading-none text-sm text-zinc-400">
Zaps received
</span>
</div>
<div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100">
{data.stats[pubkey].zaps_sent
? compactNumber.format(data.stats[pubkey].zaps_sent.msats / 1000)
: 0}
</span>
<span className="leading-none text-sm text-zinc-400">Zaps sent</span>
</div>
</div>
);
}

View File

@@ -1,47 +1,25 @@
import { usePublish } from "@libs/ndk";
import { UserFeed } from "@app/user/components/feed";
import { UserMetadata } from "@app/user/components/metadata";
import { Tab } from "@headlessui/react";
import { ThreadsIcon, ZapIcon } from "@shared/icons";
import { Image } from "@shared/image";
import { DEFAULT_AVATAR } from "@stores/constants";
import { useQuery } from "@tanstack/react-query";
import { useFollows } from "@utils/hooks/useFollows";
import { useProfile } from "@utils/hooks/useProfile";
import { compactNumber } from "@utils/number";
import { useSocial } from "@utils/hooks/useSocial";
import { shortenKey } from "@utils/shortenKey";
import { useEffect, useState } from "react";
import { Fragment, useEffect, useState } from "react";
import { Link, useParams } from "react-router-dom";
export function UserScreen() {
const publish = usePublish();
const [followed, setFollowed] = useState(false);
const { pubkey } = useParams();
const { user } = useProfile(pubkey);
const { status: followsStatus, follows } = useFollows();
const {
status: userStatsStatus,
data: userStats,
error,
} = useQuery(["user", pubkey], async () => {
const res = await fetch(
`https://api.nostr.band/v0/stats/profile/${pubkey}`,
);
if (!res.ok) {
throw new Error("Error");
}
return await res.json();
});
const { status, userFollows, follow, unfollow } = useSocial();
const follow = (pubkey: string) => {
const [followed, setFollowed] = useState(false);
const followUser = (pubkey: string) => {
try {
const followsAsSet = new Set(follows);
followsAsSet.add(pubkey);
const tags = [];
followsAsSet.forEach((item) => {
tags.push(["p", item]);
});
// publish event
publish({ content: "", kind: 3, tags: tags });
follow(pubkey);
// update state
setFollowed(true);
@@ -50,18 +28,9 @@ export function UserScreen() {
}
};
const unfollow = (pubkey: string) => {
const unfollowUser = (pubkey: string) => {
try {
const followsAsSet = new Set(follows);
followsAsSet.delete(pubkey);
const tags = [];
followsAsSet.forEach((item) => {
tags.push(["p", item]);
});
// publish event
publish({ content: "", kind: 3, tags: tags });
unfollow(pubkey);
// update state
setFollowed(false);
@@ -71,15 +40,15 @@ export function UserScreen() {
};
useEffect(() => {
if (followsStatus === "success" && follows) {
if (follows.includes(pubkey)) {
if (status === "success" && userFollows) {
if (userFollows.includes(pubkey)) {
setFollowed(true);
}
}
}, [followsStatus]);
}, [status]);
return (
<div className="h-full w-full">
<div className="h-full w-full overflow-y-auto">
<div
data-tauri-drag-region
className="h-11 w-full flex items-center px-3 border-b border-zinc-900"
@@ -92,107 +61,97 @@ export function UserScreen() {
className="w-full h-full object-cover"
/>
</div>
<div className="w-full px-5 -mt-7">
<div>
<div className="w-full -mt-7">
<div className="px-5">
<Image
src={user?.image}
fallback={DEFAULT_AVATAR}
alt={pubkey}
className="w-14 h-14 rounded-md ring-2 ring-black"
/>
<div className="flex-1 flex flex-col gap-2 mt-4">
<h5 className="font-semibold leading-none">
{user?.displayName || user?.name || "No name"}
</h5>
<span className="max-w-[15rem] text-sm truncate leading-none text-zinc-500">
{user?.nip05 || shortenKey(pubkey)}
</span>
<p className="mt-1 line-clamp-3 break-words leading-tight text-zinc-100">
{user?.about}
</p>
</div>
</div>
<div className="mt-8">
{error && <p>Failed to fetch user stats</p>}
{userStatsStatus === "loading" ? (
<p>Loading...</p>
) : (
<div className="w-full flex items-center gap-10">
<div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100">
{userStats.stats[pubkey].followers_pubkey_count ?? 0}
</span>
<span className="leading-none text-sm text-zinc-400">
Followers
<div className="flex-1 flex flex-col gap-4 mt-2">
<div className="flex items-center gap-16">
<div className="inline-flex flex-col gap-1.5">
<h5 className="font-semibold text-lg leading-none">
{user?.displayName || user?.name || "No name"}
</h5>
<span className="max-w-[15rem] text-sm truncate leading-none text-zinc-500">
{user?.nip05 || shortenKey(pubkey)}
</span>
</div>
<div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100">
{userStats.stats[pubkey].pub_following_pubkey_count ?? 0}
</span>
<span className="leading-none text-sm text-zinc-400">
Following
</span>
</div>
<div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100">
{userStats.stats[pubkey].zaps_received
? compactNumber.format(
userStats.stats[pubkey].zaps_received.msats / 1000,
)
: 0}
</span>
<span className="leading-none text-sm text-zinc-400">
Zaps received
</span>
</div>
<div className="inline-flex flex-col gap-1">
<span className="leading-none font-semibold text-zinc-100">
{userStats.stats[pubkey].zaps_sent
? compactNumber.format(
userStats.stats[pubkey].zaps_sent.msats / 1000,
)
: 0}
</span>
<span className="leading-none text-sm text-zinc-400">
Zaps sent
</span>
<div className="inline-flex items-center gap-2">
{status === "loading" ? (
<button
type="button"
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
>
Loading...
</button>
) : followed ? (
<button
type="button"
onClick={() => unfollowUser(pubkey)}
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
>
Unfollow
</button>
) : (
<button
type="button"
onClick={() => followUser(pubkey)}
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
>
Follow
</button>
)}
<Link
to={`/app/chat/${pubkey}`}
className="inline-flex w-36 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
>
Message
</Link>
<button
type="button"
className="inline-flex w-10 h-10 items-center justify-center rounded-md bg-zinc-900 group hover:bg-orange-500 text-sm font-medium"
>
<ZapIcon className="w-5 h-5" />
</button>
</div>
</div>
)}
<div className="mt-6 flex items-center gap-2">
{followsStatus === "loading" ? (
<button
type="button"
className="inline-flex w-44 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
>
Loading...
</button>
) : followed ? (
<button
type="button"
onClick={() => unfollow(pubkey)}
className="inline-flex w-44 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
>
Unfollow
</button>
) : (
<button
type="button"
onClick={() => follow(pubkey)}
className="inline-flex w-44 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
>
Follow
</button>
)}
<Link
to={`/app/chat/${pubkey}`}
className="inline-flex w-44 h-10 items-center justify-center rounded-md bg-zinc-900 hover:bg-fuchsia-500 text-sm font-medium"
>
Message
</Link>
<div className="flex flex-col gap-8">
<p className="mt-2 max-w-[500px] break-words select-text text-zinc-100">
{user?.about}
</p>
<UserMetadata pubkey={pubkey} />
</div>
</div>
</div>
<div className="mt-8 w-full border-t border-zinc-900">
<Tab.Group>
<Tab.List className="px-5 mb-2">
<Tab as={Fragment}>
{({ selected }) => (
<button
type="button"
className={`${
selected
? "text-fuchsia-500 border-fuchsia-500"
: "text-zinc-200 border-transparent"
} font-medium inline-flex items-center gap-2 h-10 border-t`}
>
<ThreadsIcon className="w-4 h-4" />
Posts
</button>
)}
</Tab>
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<UserFeed pubkey={pubkey} />
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</div>
</div>
);