mirror of
https://github.com/luminous-devs/lume.git
synced 2024-09-18 11:13:30 +00:00
wip: browse user
This commit is contained in:
parent
18a9ba3fb0
commit
c9bc7b81dd
23
src/app.tsx
23
src/app.tsx
@ -4,6 +4,7 @@ import { RouterProvider, createBrowserRouter, redirect } from 'react-router-dom'
|
|||||||
import { AuthCreateScreen } from '@app/auth/create';
|
import { AuthCreateScreen } from '@app/auth/create';
|
||||||
import { AuthImportScreen } from '@app/auth/import';
|
import { AuthImportScreen } from '@app/auth/import';
|
||||||
import { OnboardingScreen } from '@app/auth/onboarding';
|
import { OnboardingScreen } from '@app/auth/onboarding';
|
||||||
|
import { BrowseScreen } from '@app/browse';
|
||||||
import { ErrorScreen } from '@app/error';
|
import { ErrorScreen } from '@app/error';
|
||||||
|
|
||||||
import { Frame } from '@shared/frame';
|
import { Frame } from '@shared/frame';
|
||||||
@ -61,10 +62,24 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'browse',
|
path: 'browse',
|
||||||
async lazy() {
|
element: <BrowseScreen />,
|
||||||
const { BrowseScreen } = await import('@app/browse');
|
errorElement: <ErrorScreen />,
|
||||||
return { Component: BrowseScreen };
|
children: [
|
||||||
},
|
{
|
||||||
|
path: '',
|
||||||
|
async lazy() {
|
||||||
|
const { BrowseUsersScreen } = await import('@app/browse/users');
|
||||||
|
return { Component: BrowseUsersScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'relays',
|
||||||
|
async lazy() {
|
||||||
|
const { BrowseRelaysScreen } = await import('@app/browse/relays');
|
||||||
|
return { Component: BrowseRelaysScreen };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'users/:pubkey',
|
path: 'users/:pubkey',
|
||||||
|
125
src/app/browse/components/userDrawer.tsx
Normal file
125
src/app/browse/components/userDrawer.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
|
import { memo, useEffect, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import { Image } from '@shared/image';
|
||||||
|
import { NIP05 } from '@shared/nip05';
|
||||||
|
import { TextNote } from '@shared/notes';
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
|
import { useNostr } from '@utils/hooks/useNostr';
|
||||||
|
import { useProfile } from '@utils/hooks/useProfile';
|
||||||
|
import { displayNpub } from '@utils/shortenKey';
|
||||||
|
|
||||||
|
export const UserDrawer = memo(function UserDrawer({ pubkey }: { pubkey: string }) {
|
||||||
|
const { db } = useStorage();
|
||||||
|
const { status, user } = useProfile(pubkey);
|
||||||
|
const { addContact, removeContact } = useNostr();
|
||||||
|
|
||||||
|
const [followed, setFollowed] = useState(false);
|
||||||
|
|
||||||
|
const followUser = (pubkey: string) => {
|
||||||
|
try {
|
||||||
|
addContact(pubkey);
|
||||||
|
|
||||||
|
// update state
|
||||||
|
setFollowed(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const unfollowUser = (pubkey: string) => {
|
||||||
|
try {
|
||||||
|
removeContact(pubkey);
|
||||||
|
|
||||||
|
// update state
|
||||||
|
setFollowed(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (db.account.follows.includes(pubkey)) {
|
||||||
|
setFollowed(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog.Root>
|
||||||
|
<Dialog.Trigger asChild>
|
||||||
|
<button type="button">
|
||||||
|
<User pubkey={pubkey} variant="avatar" />
|
||||||
|
</button>
|
||||||
|
</Dialog.Trigger>
|
||||||
|
<Dialog.Portal>
|
||||||
|
<Dialog.Content className="fixed right-0 top-0 z-50 flex h-full w-[400px] items-center justify-center">
|
||||||
|
<div className="h-full w-full overflow-y-auto border-l border-white/10 bg-white/20 px-3 py-3 backdrop-blur-xl">
|
||||||
|
{status === 'loading' ? (
|
||||||
|
<div>
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Image
|
||||||
|
src={user?.picture || user?.image}
|
||||||
|
alt={pubkey}
|
||||||
|
className="h-14 w-14 rounded-lg"
|
||||||
|
/>
|
||||||
|
<div className="mt-2 flex flex-1 flex-col gap-2">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<h5 className="text-lg font-semibold leading-none">
|
||||||
|
{user?.displayName || user?.name || 'No name'}
|
||||||
|
</h5>
|
||||||
|
{user?.nip05 ? (
|
||||||
|
<NIP05
|
||||||
|
pubkey={pubkey}
|
||||||
|
nip05={user?.nip05}
|
||||||
|
className="max-w-[15rem] truncate text-sm leading-none text-white/50"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className="max-w-[15rem] truncate text-sm leading-none text-white/50">
|
||||||
|
{displayNpub(pubkey, 16)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{user.about ? <TextNote content={user.about} /> : null}
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 inline-flex items-center gap-2">
|
||||||
|
{followed ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => unfollowUser(pubkey)}
|
||||||
|
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium backdrop-blur-xl hover:bg-fuchsia-500"
|
||||||
|
>
|
||||||
|
Unfollow
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => followUser(pubkey)}
|
||||||
|
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium backdrop-blur-xl hover:bg-fuchsia-500"
|
||||||
|
>
|
||||||
|
Follow
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<Link
|
||||||
|
to={`/chats/${pubkey}`}
|
||||||
|
className="inline-flex h-10 w-36 items-center justify-center rounded-md bg-white/10 text-sm font-medium backdrop-blur-xl hover:bg-fuchsia-500"
|
||||||
|
>
|
||||||
|
Message
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Portal>
|
||||||
|
</Dialog.Root>
|
||||||
|
);
|
||||||
|
});
|
@ -1,7 +1,38 @@
|
|||||||
|
import { NavLink, Outlet } from 'react-router-dom';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
export function BrowseScreen() {
|
export function BrowseScreen() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="relative h-full w-full">
|
||||||
<p>TODO</p>
|
<div className="absolute left-0 right-0 top-4 flex w-full items-center justify-between px-3">
|
||||||
|
<div className="w-10" />
|
||||||
|
<div className="inline-flex gap-1 rounded-full border-t border-white/10 bg-white/20 p-1">
|
||||||
|
<NavLink
|
||||||
|
to="/browse/"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
twMerge(
|
||||||
|
'inline-flex h-8 w-20 items-center justify-center rounded-full text-sm font-semibold',
|
||||||
|
isActive ? 'bg-white/10 hover:bg-white/20' : ' hover:bg-white/5'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Users
|
||||||
|
</NavLink>
|
||||||
|
<NavLink
|
||||||
|
to="/browse/relays"
|
||||||
|
className={({ isActive }) =>
|
||||||
|
twMerge(
|
||||||
|
'inline-flex h-8 w-20 items-center justify-center rounded-full text-sm font-semibold',
|
||||||
|
isActive ? 'bg-white/10 hover:bg-white/20' : ' hover:bg-white/5'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Relays
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
|
<div className="w-10" />
|
||||||
|
</div>
|
||||||
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
7
src/app/browse/relays.tsx
Normal file
7
src/app/browse/relays.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export function BrowseRelaysScreen() {
|
||||||
|
return (
|
||||||
|
<div className="h-full w-full">
|
||||||
|
<p>TODO</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
22
src/app/browse/users.tsx
Normal file
22
src/app/browse/users.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { UserDrawer } from '@app/browse/components/userDrawer';
|
||||||
|
|
||||||
|
import { useStorage } from '@libs/storage/provider';
|
||||||
|
|
||||||
|
import { User } from '@shared/user';
|
||||||
|
|
||||||
|
export function BrowseUsersScreen() {
|
||||||
|
const { db } = useStorage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<div>
|
||||||
|
<User pubkey={db.account.pubkey} variant="avatar" />
|
||||||
|
<div className="mt-4 flex flex-col gap-2">
|
||||||
|
{db.account.follows.map((user) => (
|
||||||
|
<UserDrawer key={user} pubkey={user} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -28,7 +28,7 @@ export function ComposerModal() {
|
|||||||
<Dialog.Trigger asChild>
|
<Dialog.Trigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex h-9 items-center gap-2 rounded-full border-t border-white/10 bg-white/20 px-4 text-sm font-semibold leading-none text-white/80 hover:bg-fuchsia-500 hover:text-white"
|
className="flex h-9 items-center gap-2 rounded-full border-t border-white/10 bg-white/20 px-4 text-sm font-semibold text-white/80 hover:bg-fuchsia-500 hover:text-white"
|
||||||
>
|
>
|
||||||
New
|
New
|
||||||
<ComposeIcon className="h-4 w-4 text-white" />
|
<ComposeIcon className="h-4 w-4 text-white" />
|
||||||
|
@ -50,7 +50,7 @@ export function Navigation() {
|
|||||||
Home
|
Home
|
||||||
</NavLink>
|
</NavLink>
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/browse"
|
to="/browse/"
|
||||||
preventScrollReset={true}
|
preventScrollReset={true}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
twMerge(
|
twMerge(
|
||||||
|
@ -20,7 +20,15 @@ export const User = memo(function User({
|
|||||||
}: {
|
}: {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
time?: number;
|
time?: number;
|
||||||
variant?: 'default' | 'simple' | 'mention' | 'repost' | 'chat' | 'large' | 'thread';
|
variant?:
|
||||||
|
| 'default'
|
||||||
|
| 'simple'
|
||||||
|
| 'mention'
|
||||||
|
| 'repost'
|
||||||
|
| 'chat'
|
||||||
|
| 'large'
|
||||||
|
| 'thread'
|
||||||
|
| 'avatar';
|
||||||
embedProfile?: string;
|
embedProfile?: string;
|
||||||
}) {
|
}) {
|
||||||
const { status, user } = useProfile(pubkey, embedProfile);
|
const { status, user } = useProfile(pubkey, embedProfile);
|
||||||
@ -125,6 +133,16 @@ export const User = memo(function User({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (variant === 'avatar') {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
src={user?.picture || user?.image}
|
||||||
|
alt={pubkey}
|
||||||
|
className="h-12 w-12 shrink-0 rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (variant === 'repost') {
|
if (variant === 'repost') {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -44,7 +44,7 @@ export function LocalThreadWidget({ params }: { params: Widget }) {
|
|||||||
return (
|
return (
|
||||||
<WidgetWrapper>
|
<WidgetWrapper>
|
||||||
<TitleBar id={params.id} title={params.title} />
|
<TitleBar id={params.id} title={params.title} />
|
||||||
<div className="h-full">
|
<div className="scrollbar-hide h-full overflow-y-auto">
|
||||||
{status === 'loading' ? (
|
{status === 'loading' ? (
|
||||||
<div className="px-3 py-1.5">
|
<div className="px-3 py-1.5">
|
||||||
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
|
<div className="rounded-xl bg-white/10 px-3 py-3 backdrop-blur-xl">
|
||||||
|
@ -25,7 +25,7 @@ export function useProfile(pubkey: string, embed?: string) {
|
|||||||
{
|
{
|
||||||
kinds: [NDKKind.Metadata],
|
kinds: [NDKKind.Metadata],
|
||||||
authors: [cleanPubkey],
|
authors: [cleanPubkey],
|
||||||
limit: 1,
|
limit: 100,
|
||||||
},
|
},
|
||||||
{ closeOnEose: true }
|
{ closeOnEose: true }
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user