wip: finish browse users
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { message } from '@tauri-apps/api/dialog';
|
||||
import { RouterProvider, createBrowserRouter, redirect } from 'react-router-dom';
|
||||
import 'reactflow/dist/style.css';
|
||||
|
||||
import { AuthCreateScreen } from '@app/auth/create';
|
||||
import { AuthImportScreen } from '@app/auth/import';
|
||||
|
||||
29
src/app/browse/components/edge.tsx
Normal file
29
src/app/browse/components/edge.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { BaseEdge, EdgeProps, getBezierPath } from 'reactflow';
|
||||
|
||||
export function Edge({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
style = {},
|
||||
markerEnd,
|
||||
}: EdgeProps) {
|
||||
const [edgePath] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
style={{ ...style, stroke: '#71717a' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
17
src/app/browse/components/groupTitle.tsx
Normal file
17
src/app/browse/components/groupTitle.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useProfile } from '@utils/hooks/useProfile';
|
||||
|
||||
export const GroupTitle = memo(function GroupTitle({ pubkey }: { pubkey: string }) {
|
||||
const { status, user } = useProfile(pubkey);
|
||||
|
||||
if (status === 'loading') {
|
||||
return <div className="h-3 w-24 animate-pulse rounded bg-white/10" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<h3 className="text-sm font-semibold text-fuchsia-500">{`${
|
||||
user.name || user.display_name
|
||||
}'s network`}</h3>
|
||||
);
|
||||
});
|
||||
14
src/app/browse/components/line.tsx
Normal file
14
src/app/browse/components/line.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
export function Line({ fromX, fromY, toX, toY }) {
|
||||
return (
|
||||
<g>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#f5d0fe"
|
||||
strokeWidth={1.5}
|
||||
className="animated"
|
||||
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
|
||||
/>
|
||||
<circle cx={toX} cy={toY} fill="#fff" r={3} stroke="#f5d0fe" strokeWidth={1.5} />
|
||||
</g>
|
||||
);
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
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 { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id: pubkey,
|
||||
});
|
||||
|
||||
const [followed, setFollowed] = useState(false);
|
||||
|
||||
const style = transform
|
||||
? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
zIndex: 20,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
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"
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
>
|
||||
<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 px-4 pb-4 pt-16">
|
||||
<div className="h-full w-full overflow-y-auto rounded-lg border-t border-white/10 bg-white/20 px-3 py-3 backdrop-blur-3xl">
|
||||
{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,22 +0,0 @@
|
||||
import { useDroppable } from '@dnd-kit/core';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { PlusIcon } from '@shared/icons';
|
||||
|
||||
export function UserDropable() {
|
||||
const { isOver, setNodeRef } = useDroppable({
|
||||
id: 'newBlock',
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={twMerge(
|
||||
'inline-flex h-12 w-12 items-center justify-center rounded-lg border-t border-white/10 backdrop-blur-xl',
|
||||
isOver ? 'bg-fuchsia-500' : 'bg-white/20 hover:bg-white/30'
|
||||
)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
src/app/browse/components/userGroupNode.tsx
Normal file
34
src/app/browse/components/userGroupNode.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
import { UserWithDrawer } from '@app/browse/components/userWithDrawer';
|
||||
|
||||
import { GroupTitle } from './groupTitle';
|
||||
|
||||
export function UserGroupNode({ data }) {
|
||||
return (
|
||||
<>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
className="h-2 w-5 rounded-full border-none !bg-fuchsia-400"
|
||||
/>
|
||||
<div className="relative mx-3 my-3 flex flex-col gap-1">
|
||||
{data.title ? (
|
||||
<h3 className="text-sm font-semibold text-fuchsia-500">{data.title}</h3>
|
||||
) : (
|
||||
<GroupTitle pubkey={data.pubkey} />
|
||||
)}
|
||||
<div className="grid grid-cols-5 gap-6 rounded-lg border border-fuchsia-500/50 bg-fuchsia-500/10 p-4">
|
||||
{data.list.map((user: string) => (
|
||||
<UserWithDrawer key={user} pubkey={user} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
className="h-2 w-5 rounded-full border-none !bg-fuchsia-400"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
80
src/app/browse/components/userLatestPosts.tsx
Normal file
80
src/app/browse/components/userLatestPosts.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { LoaderIcon } from '@shared/icons';
|
||||
import {
|
||||
ArticleNote,
|
||||
FileNote,
|
||||
NoteWrapper,
|
||||
Repost,
|
||||
TextNote,
|
||||
UnknownNote,
|
||||
} from '@shared/notes';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
|
||||
export function UserLatestPosts({ pubkey }: { pubkey: string }) {
|
||||
const { getEventsByPubkey } = useNostr();
|
||||
const { status, data } = useQuery(['user-posts', pubkey], async () => {
|
||||
return await getEventsByPubkey(pubkey);
|
||||
});
|
||||
|
||||
const renderItem = useCallback(
|
||||
(event: NDKEvent) => {
|
||||
switch (event.kind) {
|
||||
case NDKKind.Text:
|
||||
return (
|
||||
<NoteWrapper key={event.id} event={event}>
|
||||
<TextNote content={event.content} />
|
||||
</NoteWrapper>
|
||||
);
|
||||
case NDKKind.Repost:
|
||||
return <Repost key={event.id} event={event} />;
|
||||
case 1063:
|
||||
return (
|
||||
<NoteWrapper key={event.id} event={event}>
|
||||
<FileNote event={event} />
|
||||
</NoteWrapper>
|
||||
);
|
||||
case NDKKind.Article:
|
||||
return (
|
||||
<NoteWrapper key={event.id} event={event}>
|
||||
<ArticleNote event={event} />
|
||||
</NoteWrapper>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<NoteWrapper key={event.id} event={event}>
|
||||
<UnknownNote event={event} />
|
||||
</NoteWrapper>
|
||||
);
|
||||
}
|
||||
},
|
||||
[data]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-4 border-t border-white/5 pt-3">
|
||||
<h3 className="mb-4 px-3 font-semibold text-white">Latest post</h3>
|
||||
<div>
|
||||
{status === 'loading' ? (
|
||||
<div className="px-3">
|
||||
<div className="inline-flex h-16 w-full items-center justify-center gap-1.5 rounded-lg bg-white/10 text-sm font-medium text-white/70">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-white" />
|
||||
Loading latest posts...
|
||||
</div>
|
||||
</div>
|
||||
) : data.length < 1 ? (
|
||||
<div className="px-3">
|
||||
<div className="inline-flex h-16 w-full items-center justify-center rounded-lg bg-white/10 text-sm font-medium text-white/70">
|
||||
No posts from 24 hours ago
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
data.map((event) => renderItem(event))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
src/app/browse/components/userNode.tsx
Normal file
21
src/app/browse/components/userNode.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
import { User } from '@shared/user';
|
||||
|
||||
export function UserNode({ data }) {
|
||||
return (
|
||||
<>
|
||||
<div className="relative mx-3 my-3 inline-flex h-12 w-12 shrink-0 items-center justify-center">
|
||||
<span className="absolute inline-flex h-8 w-8 animate-ping rounded-lg bg-green-400 opacity-75"></span>
|
||||
<div className="relative z-10">
|
||||
<User pubkey={data.pubkey} variant="avatar" />
|
||||
</div>
|
||||
</div>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Bottom}
|
||||
className="h-2 w-2 rounded-full border-none !bg-white/20"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
130
src/app/browse/components/userWithDrawer.tsx
Normal file
130
src/app/browse/components/userWithDrawer.tsx
Normal file
@@ -0,0 +1,130 @@
|
||||
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';
|
||||
|
||||
import { UserLatestPosts } from './userLatestPosts';
|
||||
|
||||
export const UserWithDrawer = memo(function UserWithDrawer({
|
||||
pubkey,
|
||||
}: {
|
||||
pubkey: string;
|
||||
}) {
|
||||
const { addContact, removeContact } = useNostr();
|
||||
const { db } = useStorage();
|
||||
const { status, user } = useProfile(pubkey);
|
||||
|
||||
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 px-4 pb-4 pt-16">
|
||||
<div className="h-full w-full overflow-y-auto rounded-lg border-t border-white/10 bg-white/20 py-3 backdrop-blur-3xl">
|
||||
{status === 'loading' ? (
|
||||
<div>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-3 px-3">
|
||||
<Image
|
||||
src={user?.picture || user?.image}
|
||||
alt={pubkey}
|
||||
className="h-12 w-12 rounded-lg"
|
||||
/>
|
||||
<div className="flex flex-1 flex-col gap-2">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<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>
|
||||
)}
|
||||
{user.about ? <TextNote content={user.about} /> : null}
|
||||
</div>
|
||||
<div className="mt-3 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>
|
||||
<UserLatestPosts pubkey={pubkey} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
});
|
||||
@@ -1,45 +1,43 @@
|
||||
import { NavLink, Outlet } from 'react-router-dom';
|
||||
import { ReactFlowProvider } from 'reactflow';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
import { DotsPattern } from '@shared/icons';
|
||||
|
||||
export function BrowseScreen() {
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<div className="absolute left-0 right-0 top-4 z-30 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 backdrop-blur-xl">
|
||||
<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>
|
||||
<ReactFlowProvider>
|
||||
<div className="relative h-full w-full">
|
||||
<div className="absolute left-0 right-0 top-4 z-30 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 backdrop-blur-xl">
|
||||
<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>
|
||||
<div className="relative z-20 h-full w-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
<div className="w-10" />
|
||||
</div>
|
||||
<div className="absolute z-10 h-full w-full">
|
||||
<DotsPattern className="h-full w-full text-white/10" />
|
||||
</div>
|
||||
<div className="relative z-20 h-full w-full">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +1,116 @@
|
||||
import { DndContext } from '@dnd-kit/core';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
ConnectionMode,
|
||||
addEdge,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
useReactFlow,
|
||||
} from 'reactflow';
|
||||
|
||||
import { UserDrawer } from '@app/browse/components/userDrawer';
|
||||
import { UserDropable } from '@app/browse/components/userDropable';
|
||||
import { Edge } from '@app/browse//components/edge';
|
||||
import { UserGroupNode } from '@app/browse//components/userGroupNode';
|
||||
import { Line } from '@app/browse/components/line';
|
||||
import { UserNode } from '@app/browse/components/userNode';
|
||||
|
||||
import { useStorage } from '@libs/storage/provider';
|
||||
|
||||
import { User } from '@shared/user';
|
||||
|
||||
import { useNostr } from '@utils/hooks/useNostr';
|
||||
import { getMultipleRandom } from '@utils/transform';
|
||||
|
||||
let id = 2;
|
||||
const getId = () => `${id++}`;
|
||||
const nodeTypes = { user: UserNode, userGroup: UserGroupNode };
|
||||
const edgeTypes = { buttonedge: Edge };
|
||||
|
||||
export function BrowseUsersScreen() {
|
||||
const { db } = useStorage();
|
||||
const { getContactsByPubkey } = useNostr();
|
||||
const { project } = useReactFlow();
|
||||
|
||||
const data = useMemo(() => getMultipleRandom(db.account.follows, 10), []);
|
||||
const defaultContacts = useMemo(() => getMultipleRandom(db.account.follows, 10), []);
|
||||
const reactFlowWrapper = useRef(null);
|
||||
const connectingNodeId = useRef(null);
|
||||
|
||||
const handleDragEnd = (event) => {
|
||||
console.log(event.id);
|
||||
};
|
||||
const initialNodes = [
|
||||
{
|
||||
id: '0',
|
||||
type: 'user',
|
||||
position: { x: 141, y: 0 },
|
||||
data: { list: [], title: '', pubkey: db.account.pubkey },
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'userGroup',
|
||||
position: { x: 0, y: 200 },
|
||||
data: { list: defaultContacts, title: 'Starting Point', pubkey: '' },
|
||||
},
|
||||
];
|
||||
const initialEdges = [{ id: 'e0-1', type: 'buttonedge', source: '0', target: '1' }];
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), []);
|
||||
|
||||
const onConnectStart = useCallback((_, { nodeId }) => {
|
||||
connectingNodeId.current = nodeId;
|
||||
}, []);
|
||||
|
||||
const onConnectEnd = useCallback(
|
||||
async (event) => {
|
||||
const targetIsPane = event.target.classList.contains('react-flow__pane');
|
||||
|
||||
if (targetIsPane) {
|
||||
const { top, left } = reactFlowWrapper.current.getBoundingClientRect();
|
||||
|
||||
const id = getId();
|
||||
const prevData = nodes.slice(-1)[0];
|
||||
const randomPubkey = getMultipleRandom(prevData.data.list, 1)[0];
|
||||
|
||||
const newContactList = await getContactsByPubkey(randomPubkey);
|
||||
const newNode = {
|
||||
id,
|
||||
type: 'userGroup',
|
||||
position: project({ x: event.clientX - left, y: event.clientY - top }),
|
||||
data: { list: newContactList, title: null, pubkey: randomPubkey },
|
||||
};
|
||||
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
setEdges((eds) =>
|
||||
eds.concat({
|
||||
id,
|
||||
type: 'buttonedge',
|
||||
source: connectingNodeId.current,
|
||||
target: id,
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
[project]
|
||||
);
|
||||
|
||||
return (
|
||||
<DndContext onDragEnd={handleDragEnd}>
|
||||
<div className="scrollbar-hide flex h-full w-full flex-col items-center justify-center overflow-x-auto overflow-y-auto">
|
||||
<div className="flex items-center gap-16">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-sm font-semibold text-fuchsia-500">Follows</h3>
|
||||
<div className="grid grid-cols-5 gap-6 rounded-lg border border-fuchsia-500/50 bg-fuchsia-500/10 p-4">
|
||||
{data.map((user) => (
|
||||
<UserDrawer key={user} pubkey={user} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-16">
|
||||
<User pubkey={db.account.pubkey} variant="avatar" />
|
||||
<UserDropable />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DndContext>
|
||||
<div className="h-full w-full" ref={reactFlowWrapper}>
|
||||
<ReactFlow
|
||||
proOptions={{ hideAttribution: true }}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
connectionLineComponent={Line}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
connectionMode={ConnectionMode.Loose}
|
||||
minZoom={0.8}
|
||||
maxZoom={1.2}
|
||||
fitView
|
||||
>
|
||||
<Background color="#3f3f46" />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ export function ChatsList() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
{chats.follows.map((item) => renderItem(item))}
|
||||
{chats.unknowns.length > 0 && <UnknownsModal data={chats.unknowns} />}
|
||||
{chats?.follows?.map((item) => renderItem(item))}
|
||||
{chats?.unknowns?.length > 0 && <UnknownsModal data={chats.unknowns} />}
|
||||
<NewMessageModal />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -24,7 +24,10 @@ export function Navigation() {
|
||||
|
||||
return (
|
||||
<Frame className="relative flex h-full w-[232px] flex-col" lighter>
|
||||
<div className="inline-flex h-16 w-full items-center justify-end px-3">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="inline-flex h-16 w-full items-center justify-end px-3"
|
||||
>
|
||||
<ComposerModal />
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -36,7 +36,9 @@ export const User = memo(function User({
|
||||
|
||||
if (status === 'loading') {
|
||||
if (variant === 'avatar') {
|
||||
<div className="h-12 w-12 animate-pulse overflow-hidden rounded-lg bg-white/10 backdrop-blur-xl" />;
|
||||
return (
|
||||
<div className="h-12 w-12 animate-pulse overflow-hidden rounded-lg bg-white/10 backdrop-blur-xl" />
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'mention') {
|
||||
|
||||
22
src/stores/browse.ts
Normal file
22
src/stores/browse.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { create } from 'zustand';
|
||||
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||
|
||||
interface BrowseState {
|
||||
data: Array<{ title: string; data: string[] }>;
|
||||
setData: ({ title, data }: { title: string; data: string[] }) => void;
|
||||
}
|
||||
|
||||
export const useBrowse = create<BrowseState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
data: [],
|
||||
setData: (data) => {
|
||||
set((state) => ({ data: [...state.data, data] }));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: 'browseUsers',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}
|
||||
)
|
||||
);
|
||||
@@ -19,6 +19,7 @@ import { useStronghold } from '@stores/stronghold';
|
||||
|
||||
import { createBlobFromFile } from '@utils/createBlobFromFile';
|
||||
import { nHoursAgo } from '@utils/date';
|
||||
import { getMultipleRandom } from '@utils/transform';
|
||||
import { NDKEventWithReplies, NostrBuildResponse } from '@utils/types';
|
||||
|
||||
export function useNostr() {
|
||||
@@ -278,6 +279,22 @@ export function useNostr() {
|
||||
return events;
|
||||
};
|
||||
|
||||
const getContactsByPubkey = async (pubkey: string) => {
|
||||
const user = ndk.getUser({ hexpubkey: pubkey });
|
||||
const follows = [...(await user.follows())].map((user) => user.hexpubkey);
|
||||
return getMultipleRandom([...follows], 10);
|
||||
};
|
||||
|
||||
const getEventsByPubkey = async (pubkey: string) => {
|
||||
const events = await fetcher.fetchAllEvents(
|
||||
relayUrls,
|
||||
{ authors: [pubkey], kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Article] },
|
||||
{ since: nHoursAgo(24) },
|
||||
{ sort: true }
|
||||
);
|
||||
return events as unknown as NDKEvent[];
|
||||
};
|
||||
|
||||
const publish = async ({
|
||||
content,
|
||||
kind,
|
||||
@@ -421,5 +438,7 @@ export function useNostr() {
|
||||
publish,
|
||||
createZap,
|
||||
upload,
|
||||
getContactsByPubkey,
|
||||
getEventsByPubkey,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user