mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
update profile and channels layouts
This commit is contained in:
parent
5b2113b3b9
commit
54a9c26901
115
src/app.tsx
115
src/app.tsx
@ -75,63 +75,66 @@ const NoLayoutPage = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "signin",
|
||||
Component: NoLayoutPage,
|
||||
children: signinRoutes,
|
||||
},
|
||||
{
|
||||
path: "signup",
|
||||
Component: NoLayoutPage,
|
||||
children: signupRoutes,
|
||||
},
|
||||
{
|
||||
Component: RootPage,
|
||||
children: [
|
||||
{ index: true, Component: HomeView },
|
||||
{ path: "notes", Component: HomeView },
|
||||
{ path: "new", children: newRoutes },
|
||||
{ path: "launchpad", Component: LaunchpadView },
|
||||
{ path: "profile", Component: ProfileView },
|
||||
{ path: "messages", children: messagesRoutes },
|
||||
{ path: "user/:pubkey", children: userRoutes },
|
||||
{ path: "u/:pubkey", children: userRoutes },
|
||||
{ path: "note/:id", Component: ThreadView },
|
||||
{ path: "n/:id", Component: ThreadView },
|
||||
{ path: "search", Component: SearchView },
|
||||
{ path: "other-stuff", Component: OtherStuffView },
|
||||
{ path: "settings", children: settingsRoutes },
|
||||
{ path: "relays", children: relaysRoutes },
|
||||
{ path: "r/:relay", Component: RelayView },
|
||||
{ path: "notifications", Component: NotificationsView },
|
||||
{ path: "media", children: mediaRoutes },
|
||||
{ path: "streams", children: streamsRoutes },
|
||||
{ path: "tools", children: toolsRoutes },
|
||||
{ path: "discovery", children: discoveryRoutes },
|
||||
{ path: "wiki", children: wikiRoutes },
|
||||
{ path: "support", Component: SupportView },
|
||||
{ path: "l/:link", Component: NostrLinkView },
|
||||
{ path: "t/:hashtag", Component: HashTagView },
|
||||
const router = createBrowserRouter(
|
||||
[
|
||||
{
|
||||
path: "signin",
|
||||
Component: NoLayoutPage,
|
||||
children: signinRoutes,
|
||||
},
|
||||
{
|
||||
path: "signup",
|
||||
Component: NoLayoutPage,
|
||||
children: signupRoutes,
|
||||
},
|
||||
{
|
||||
Component: RootPage,
|
||||
children: [
|
||||
{ index: true, Component: HomeView },
|
||||
{ path: "notes", Component: HomeView },
|
||||
{ path: "new", children: newRoutes },
|
||||
{ path: "launchpad", Component: LaunchpadView },
|
||||
{ path: "profile", Component: ProfileView },
|
||||
{ path: "messages", children: messagesRoutes },
|
||||
{ path: "user/:pubkey", children: userRoutes },
|
||||
{ path: "u/:pubkey", children: userRoutes },
|
||||
{ path: "note/:id", Component: ThreadView },
|
||||
{ path: "n/:id", Component: ThreadView },
|
||||
{ path: "search", Component: SearchView },
|
||||
{ path: "other-stuff", Component: OtherStuffView },
|
||||
{ path: "settings", children: settingsRoutes },
|
||||
{ path: "relays", children: relaysRoutes },
|
||||
{ path: "r/:relay", Component: RelayView },
|
||||
{ path: "notifications", Component: NotificationsView },
|
||||
{ path: "media", children: mediaRoutes },
|
||||
{ path: "streams", children: streamsRoutes },
|
||||
{ path: "tools", children: toolsRoutes },
|
||||
{ path: "discovery", children: discoveryRoutes },
|
||||
{ path: "wiki", children: wikiRoutes },
|
||||
{ path: "support", Component: SupportView },
|
||||
{ path: "l/:link", Component: NostrLinkView },
|
||||
{ path: "t/:hashtag", Component: HashTagView },
|
||||
|
||||
// other stuff
|
||||
{ path: "articles", children: articlesRoutes },
|
||||
{ path: "bookmarks", children: bookmarksRoutes },
|
||||
{ path: "lists", children: listsRoutes },
|
||||
{ path: "files", children: filesRoutes },
|
||||
{ path: "tracks", Component: TracksView },
|
||||
{ path: "map", Component: MapView },
|
||||
{ path: "videos", children: videosRoutes },
|
||||
{ path: "torrents", children: torrentsRoutes },
|
||||
{ path: "channels", children: channelsRoutes },
|
||||
{ path: "goals", children: goalsRoutes },
|
||||
{ path: "badges", children: badgesRoutes },
|
||||
{ path: "emojis", children: emojisRoutes },
|
||||
{ path: "wallet", children: walletRoutes },
|
||||
{ path: "podcasts", children: podcastsRoutes },
|
||||
],
|
||||
},
|
||||
]);
|
||||
// other stuff
|
||||
{ path: "articles", children: articlesRoutes },
|
||||
{ path: "bookmarks", children: bookmarksRoutes },
|
||||
{ path: "lists", children: listsRoutes },
|
||||
{ path: "files", children: filesRoutes },
|
||||
{ path: "tracks", Component: TracksView },
|
||||
{ path: "map", Component: MapView },
|
||||
{ path: "videos", children: videosRoutes },
|
||||
{ path: "torrents", children: torrentsRoutes },
|
||||
{ path: "channels", children: channelsRoutes },
|
||||
{ path: "goals", children: goalsRoutes },
|
||||
{ path: "badges", children: badgesRoutes },
|
||||
{ path: "emojis", children: emojisRoutes },
|
||||
{ path: "wallet", children: walletRoutes },
|
||||
{ path: "podcasts", children: podcastsRoutes },
|
||||
],
|
||||
},
|
||||
],
|
||||
{ future: { v7_relativeSplatPath: true } },
|
||||
);
|
||||
|
||||
export const App = () => (
|
||||
<ErrorBoundary>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PropsWithChildren, Suspense } from "react";
|
||||
import { PropsWithChildren, ReactNode, Suspense } from "react";
|
||||
import { Outlet, useMatch } from "react-router-dom";
|
||||
import { Flex, Spinner } from "@chakra-ui/react";
|
||||
|
||||
@ -11,7 +11,8 @@ export default function ContainedParentView({
|
||||
path,
|
||||
title,
|
||||
width = "xs",
|
||||
}: PropsWithChildren<{ path: string; title?: string; width?: "xs" | "sm" | "md" }>) {
|
||||
actions,
|
||||
}: PropsWithChildren<{ path: string; title?: string; width?: "xs" | "sm" | "md"; actions?: ReactNode }>) {
|
||||
const match = useMatch(path);
|
||||
const isMobile = useBreakpointValue({ base: true, lg: false });
|
||||
const showMenu = !isMobile || !!match;
|
||||
@ -19,8 +20,15 @@ export default function ContainedParentView({
|
||||
return (
|
||||
<Flex data-type="contained-view" h="100vh" overflow="hidden" direction={{ base: "column", lg: "row" }}>
|
||||
{showMenu && (
|
||||
<Flex w={{ base: "full", lg: width }} direction="column" overflowY="auto" overflowX="hidden" h="full">
|
||||
{title && <SimpleHeader title={title} />}
|
||||
<Flex
|
||||
w={{ base: "full", lg: width }}
|
||||
direction="column"
|
||||
overflowY="auto"
|
||||
overflowX="hidden"
|
||||
h="full"
|
||||
flexShrink={0}
|
||||
>
|
||||
{title && <SimpleHeader title={title}>{actions}</SimpleHeader>}
|
||||
<Flex direction="column" p="2" gap="2">
|
||||
{children}
|
||||
</Flex>
|
||||
|
54
src/components/layout/presets/contained-simple-view.tsx
Normal file
54
src/components/layout/presets/contained-simple-view.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Flex, FlexProps } from "@chakra-ui/react";
|
||||
|
||||
import SimpleHeader from "./simple-header";
|
||||
|
||||
export default function ContainedSimpleView({
|
||||
children,
|
||||
actions,
|
||||
title,
|
||||
as,
|
||||
flush,
|
||||
gap,
|
||||
reverse,
|
||||
bottom,
|
||||
...props
|
||||
}: Omit<FlexProps, "title" | "bottom"> & {
|
||||
flush?: boolean;
|
||||
actions?: ReactNode;
|
||||
title?: ReactNode;
|
||||
reverse?: boolean;
|
||||
bottom?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Flex
|
||||
className="contained-simple-view"
|
||||
as={as}
|
||||
direction="column"
|
||||
pr="var(--safe-right)"
|
||||
pl="var(--safe-left)"
|
||||
h="100vh"
|
||||
overflow="hidden"
|
||||
w="full"
|
||||
{...props}
|
||||
>
|
||||
<SimpleHeader title={title}>{actions}</SimpleHeader>
|
||||
|
||||
<Flex
|
||||
direction={reverse ? "column-reverse" : "column"}
|
||||
px={flush ? 0 : "4"}
|
||||
pt={flush ? 0 : "4"}
|
||||
pb={flush ? 0 : "max(1rem, var(--safe-bottom))"}
|
||||
gap={gap || "2"}
|
||||
flexGrow={1}
|
||||
h={0}
|
||||
overflowX="hidden"
|
||||
overflowY="auto"
|
||||
>
|
||||
{children}
|
||||
</Flex>
|
||||
|
||||
{bottom}
|
||||
</Flex>
|
||||
);
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { PropsWithChildren, Suspense } from "react";
|
||||
import { Outlet, useMatch } from "react-router-dom";
|
||||
import { Outlet, OutletProps, useMatch } from "react-router-dom";
|
||||
import { Box, Flex, Spinner } from "@chakra-ui/react";
|
||||
|
||||
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
|
||||
@ -11,7 +11,8 @@ export default function SimpleParentView({
|
||||
path,
|
||||
title,
|
||||
width = "xs",
|
||||
}: PropsWithChildren<{ path: string; title?: string; width?: "xs" | "sm" | "md" }>) {
|
||||
context,
|
||||
}: PropsWithChildren<{ path: string; title?: string; width?: "xs" | "sm" | "md"; context?: OutletProps["context"] }>) {
|
||||
const match = useMatch(path);
|
||||
const isMobile = useBreakpointValue({ base: true, lg: false });
|
||||
const showMenu = !isMobile || !!match;
|
||||
@ -37,7 +38,7 @@ export default function SimpleParentView({
|
||||
{!isMobile && (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<ErrorBoundary>
|
||||
<Outlet />
|
||||
<Outlet context={context} />
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
)}
|
||||
@ -47,7 +48,7 @@ export default function SimpleParentView({
|
||||
return (
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<ErrorBoundary>
|
||||
<Outlet />
|
||||
<Outlet context={context} />
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
);
|
||||
|
27
src/components/reactions/insert-reaction-button.tsx
Normal file
27
src/components/reactions/insert-reaction-button.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { useCallback } from "react";
|
||||
import { IconButtonProps } from "@chakra-ui/react";
|
||||
import { Emoji } from "applesauce-core/helpers";
|
||||
|
||||
import ReactionIconButton from "./reaction-icon-button";
|
||||
|
||||
export default function InsertReactionButton({
|
||||
onSelect,
|
||||
...props
|
||||
}: Omit<IconButtonProps, "icon" | "onSelect"> & {
|
||||
onSelect?: (emojiCode: string, emoji?: string | Emoji) => void;
|
||||
}) {
|
||||
const handleSelect = useCallback(
|
||||
(emoji: Emoji | string) => {
|
||||
if (!onSelect) return;
|
||||
if (typeof emoji === "string") onSelect(emoji, emoji);
|
||||
else onSelect(`:${emoji.name}:`, emoji);
|
||||
},
|
||||
[onSelect],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReactionIconButton onSelect={handleSelect} {...props} />
|
||||
</>
|
||||
);
|
||||
}
|
1
src/types/nostr-extensions.d.ts
vendored
1
src/types/nostr-extensions.d.ts
vendored
@ -1,4 +1,3 @@
|
||||
import { EventTemplate, NostrEvent, UnsignedEvent, VerifiedEvent } from "nostr-tools";
|
||||
import { Nip07Interface } from "applesauce-signer";
|
||||
|
||||
declare global {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { memo, useCallback, useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Button, Flex, Heading, Spacer, Spinner, useDisclosure } from "@chakra-ui/react";
|
||||
import { Button, ButtonGroup, Flex, Heading, Spacer, Spinner, useDisclosure } from "@chakra-ui/react";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { ChannelHiddenQuery, ChannelMessagesQuery, ChannelMutedQuery } from "applesauce-channel/queries";
|
||||
import { useStoreQuery } from "applesauce-react/hooks";
|
||||
@ -26,6 +26,9 @@ import ChannelMessageForm from "./components/send-message-form";
|
||||
import useParamsEventPointer from "../../hooks/use-params-event-pointer";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import { truncateId } from "../../helpers/string";
|
||||
import SimpleView from "../../components/layout/presets/simple-view";
|
||||
import ContainedSimpleView from "../../components/layout/presets/contained-simple-view";
|
||||
import ChannelImage from "./components/channel-image";
|
||||
|
||||
const ChannelChatLog = memo(({ timeline, channel }: { timeline: TimelineLoader; channel: NostrEvent }) => {
|
||||
const messages = useStoreQuery(ChannelMessagesQuery, [channel]) ?? [];
|
||||
@ -82,36 +85,26 @@ function ChannelPage({ channel }: { channel: NostrEvent }) {
|
||||
return (
|
||||
<ThreadsProvider timeline={loader}>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Flex h="full" overflow="hidden" direction="column" p="2" gap="2" flexGrow={1}>
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Button leftIcon={<ChevronLeftIcon />} onClick={() => navigate(-1)}>
|
||||
Back
|
||||
</Button>
|
||||
<Heading hideBelow="lg" size="lg">
|
||||
<ContainedSimpleView
|
||||
reverse
|
||||
title={
|
||||
<Flex gap="2" alignItems="center">
|
||||
<ChannelImage channel={channel} w="10" rounded="md" />
|
||||
{metadata?.name}
|
||||
</Heading>
|
||||
<Spacer />
|
||||
<ChannelJoinButton channel={channel} hideBelow="lg" />
|
||||
<Button onClick={drawer.onOpen}>Channel Info</Button>
|
||||
<ChannelMenu channel={channel} aria-label="More Options" />
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
h="0"
|
||||
flexGrow={1}
|
||||
overflowX="hidden"
|
||||
overflowY="scroll"
|
||||
direction="column-reverse"
|
||||
gap="2"
|
||||
py="4"
|
||||
px="2"
|
||||
>
|
||||
<ChannelChatLog timeline={loader} channel={channel} />
|
||||
<TimelineActionAndStatus timeline={loader} />
|
||||
</Flex>
|
||||
|
||||
<ChannelMessageForm channel={channel} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
}
|
||||
actions={
|
||||
<ButtonGroup size="sm" ms="auto">
|
||||
<ChannelJoinButton channel={channel} hideBelow="lg" />
|
||||
<Button onClick={drawer.onOpen}>Channel Info</Button>
|
||||
<ChannelMenu channel={channel} aria-label="More Options" />
|
||||
</ButtonGroup>
|
||||
}
|
||||
bottom={<ChannelMessageForm channel={channel} p="2" />}
|
||||
>
|
||||
<ChannelChatLog timeline={loader} channel={channel} />
|
||||
<TimelineActionAndStatus timeline={loader} />
|
||||
</ContainedSimpleView>
|
||||
{drawer.isOpen && <ChannelMetadataDrawer isOpen onClose={drawer.onClose} channel={channel} size="lg" />}
|
||||
</IntersectionObserverProvider>
|
||||
</ThreadsProvider>
|
||||
|
@ -1,28 +1,15 @@
|
||||
import { Link as RouterLink } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { EventPointer } from "nostr-tools/nip19";
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardProps,
|
||||
Flex,
|
||||
Heading,
|
||||
LinkBox,
|
||||
Spinner,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { Card, CardBody, CardHeader, CardProps, Flex, Heading, LinkBox, Spinner, Text } from "@chakra-ui/react";
|
||||
|
||||
import useChannelMetadata from "../../../hooks/use-channel-metadata";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import HoverLinkOverlay from "../../../components/hover-link-overlay";
|
||||
import UserAvatarLink from "../../../components/user/user-avatar-link";
|
||||
import UserLink from "../../../components/user/user-link";
|
||||
import useSingleEvent from "../../../hooks/use-single-event";
|
||||
import { useReadRelays } from "../../../hooks/use-client-relays";
|
||||
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
|
||||
import ChannelImage from "./channel-image";
|
||||
|
||||
export default function ChannelCard({
|
||||
channel,
|
||||
@ -38,15 +25,7 @@ export default function ChannelCard({
|
||||
|
||||
return (
|
||||
<Card as={LinkBox} flexDirection="row" gap="2" overflow="hidden" alignItems="flex-start" ref={ref} {...props}>
|
||||
<Box
|
||||
backgroundImage={metadata.picture}
|
||||
backgroundSize="cover"
|
||||
backgroundPosition="center"
|
||||
backgroundRepeat="no-repeat"
|
||||
aspectRatio={1}
|
||||
w="7rem"
|
||||
flexShrink={0}
|
||||
/>
|
||||
<ChannelImage channel={channel} w="5rem" flexShrink={0} />
|
||||
<Flex direction="column" flex={1} overflow="hidden" h="full">
|
||||
<CardHeader p="2" display="flex" gap="2" alignItems="center">
|
||||
<Heading size="md" isTruncated>
|
||||
@ -58,10 +37,6 @@ export default function ChannelCard({
|
||||
<CardBody px="2" py="0" overflow="hidden" flexGrow={1}>
|
||||
<Text isTruncated>{metadata.about}</Text>
|
||||
</CardBody>
|
||||
<CardFooter p="2" gap="2">
|
||||
<UserAvatarLink pubkey={channel.pubkey} size="xs" />
|
||||
<UserLink pubkey={channel.pubkey} fontWeight="bold" />
|
||||
</CardFooter>
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
|
21
src/views/channels/components/channel-image.tsx
Normal file
21
src/views/channels/components/channel-image.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { NostrEvent } from "nostr-tools";
|
||||
import { Box, BoxProps } from "@chakra-ui/react";
|
||||
|
||||
import { useReadRelays } from "../../../hooks/use-client-relays";
|
||||
import useChannelMetadata from "../../../hooks/use-channel-metadata";
|
||||
|
||||
export default function ChannelImage({ channel, ...props }: { channel: NostrEvent } & Omit<BoxProps, "children">) {
|
||||
const readRelays = useReadRelays();
|
||||
const metadata = useChannelMetadata(channel.id, readRelays);
|
||||
|
||||
return (
|
||||
<Box
|
||||
backgroundImage={metadata?.picture}
|
||||
backgroundSize="cover"
|
||||
backgroundPosition="center"
|
||||
backgroundRepeat="no-repeat"
|
||||
aspectRatio={1}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -13,6 +13,8 @@ import { useContextEmojis } from "../../../providers/global/emoji-provider";
|
||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
import InsertGifButton from "../../../components/gif/insert-gif-button";
|
||||
import InsertImageButton from "../../new/note/insert-image-button";
|
||||
import ReactionIconButton from "../../../components/reactions/reaction-icon-button";
|
||||
import InsertReactionButton from "../../../components/reactions/insert-reaction-button";
|
||||
|
||||
export default function ChannelMessageForm({
|
||||
channel,
|
||||
@ -82,11 +84,12 @@ export default function ChannelMessageForm({
|
||||
}}
|
||||
/>
|
||||
<Flex gap="2" direction="column">
|
||||
<Button type="submit">Send</Button>
|
||||
<ButtonGroup size="sm">
|
||||
<InsertImageButton onUploaded={insertText} aria-label="Upload image" />
|
||||
<InsertGifButton onSelectURL={insertText} aria-label="Add gif" />
|
||||
<InsertReactionButton onSelect={insertText} aria-label="Add emoji" />
|
||||
</ButtonGroup>
|
||||
<Button type="submit">Send</Button>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
59
src/views/channels/explore.tsx
Normal file
59
src/views/channels/explore.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { useCallback } from "react";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { ButtonGroup, Flex, SimpleGrid } from "@chakra-ui/react";
|
||||
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
|
||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||
import ChannelCard from "./components/channel-card";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import SimpleView from "../../components/layout/presets/simple-view";
|
||||
import ContainedSimpleView from "../../components/layout/presets/contained-simple-view";
|
||||
|
||||
function ChannelsExplorePage() {
|
||||
const relays = useReadRelays();
|
||||
const { filter, listId } = usePeopleListContext();
|
||||
|
||||
const clientMuteFilter = useClientSideMuteFilter();
|
||||
const eventFilter = useCallback(
|
||||
(e: NostrEvent) => {
|
||||
if (clientMuteFilter(e)) return false;
|
||||
return true;
|
||||
},
|
||||
[clientMuteFilter],
|
||||
);
|
||||
const { loader, timeline: channels } = useTimelineLoader(
|
||||
`${listId}-channels`,
|
||||
relays,
|
||||
filter ? { ...filter, kinds: [kinds.ChannelCreation] } : undefined,
|
||||
{ eventFilter },
|
||||
);
|
||||
const callback = useTimelineCurserIntersectionCallback(loader);
|
||||
|
||||
return (
|
||||
<ContainedSimpleView title="Explore channels" actions={<PeopleListSelection ms="auto" size="sm" />}>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<SimpleGrid columns={{ base: 1, xl: 2, "2xl": 3 }} spacing="2">
|
||||
{channels?.map((channel) => (
|
||||
<ErrorBoundary key={channel.id}>
|
||||
<ChannelCard channel={channel} additionalRelays={relays} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</IntersectionObserverProvider>
|
||||
</ContainedSimpleView>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ChannelsExploreView() {
|
||||
return (
|
||||
<PeopleListProvider>
|
||||
<ChannelsExplorePage />
|
||||
</PeopleListProvider>
|
||||
);
|
||||
}
|
@ -1,61 +1,65 @@
|
||||
import { useCallback } from "react";
|
||||
import { kinds } from "nostr-tools";
|
||||
import { Flex, SimpleGrid } from "@chakra-ui/react";
|
||||
import { Alert, AlertDescription, AlertIcon, AlertTitle, Box, Button, Link } from "@chakra-ui/react";
|
||||
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
|
||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||
import ChannelCard from "./components/channel-card";
|
||||
import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
|
||||
function ChannelsHomePage() {
|
||||
const relays = useReadRelays();
|
||||
const { filter, listId } = usePeopleListContext();
|
||||
|
||||
const clientMuteFilter = useClientSideMuteFilter();
|
||||
const eventFilter = useCallback(
|
||||
(e: NostrEvent) => {
|
||||
if (clientMuteFilter(e)) return false;
|
||||
return true;
|
||||
},
|
||||
[clientMuteFilter],
|
||||
);
|
||||
const { loader, timeline: channels } = useTimelineLoader(
|
||||
`${listId}-channels`,
|
||||
relays,
|
||||
filter ? { ...filter, kinds: [kinds.ChannelCreation] } : undefined,
|
||||
{ eventFilter },
|
||||
);
|
||||
const callback = useTimelineCurserIntersectionCallback(loader);
|
||||
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
<Flex gap="2">
|
||||
<PeopleListSelection />
|
||||
</Flex>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<SimpleGrid columns={{ base: 1, xl: 2 }} spacing="2">
|
||||
{channels?.map((channel) => (
|
||||
<ErrorBoundary key={channel.id}>
|
||||
<ChannelCard channel={channel} additionalRelays={relays} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</IntersectionObserverProvider>
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
||||
import ContainedParentView from "../../components/layout/presets/contained-parent-view";
|
||||
import useUserChannelsList from "../../hooks/use-user-channels-list";
|
||||
import useSingleEvents from "../../hooks/use-single-events";
|
||||
import RouterLink from "../../components/router-link";
|
||||
|
||||
export default function ChannelsHomeView() {
|
||||
const relays = useReadRelays();
|
||||
const { pointers } = useUserChannelsList();
|
||||
const channels = useSingleEvents(pointers.map((p) => p.id));
|
||||
|
||||
return (
|
||||
<PeopleListProvider>
|
||||
<ChannelsHomePage />
|
||||
</PeopleListProvider>
|
||||
<ContainedParentView
|
||||
title="Public channels"
|
||||
path="/channels"
|
||||
width="sm"
|
||||
actions={
|
||||
<Button as={RouterLink} to="explore" ms="auto" size="sm">
|
||||
Explore
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Alert status="info">
|
||||
<AlertIcon />
|
||||
<Box>
|
||||
<AlertTitle>Deprecated</AlertTitle>
|
||||
<AlertDescription>
|
||||
<Link href="https://github.com/nostr-protocol/nips/blob/master/28.md">NIP-28</Link> public channels a
|
||||
deprecated in favor of <Link href="https://github.com/nostr-protocol/nips/blob/master/29.md">NIP-29</Link>{" "}
|
||||
relay based groups
|
||||
</AlertDescription>
|
||||
</Box>
|
||||
</Alert>
|
||||
{channels?.map((channel) => (
|
||||
<ErrorBoundary key={channel.id}>
|
||||
<ChannelCard channel={channel} additionalRelays={relays} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
|
||||
{channels.length === 0 && (
|
||||
<Alert
|
||||
status="info"
|
||||
variant="subtle"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
textAlign="center"
|
||||
>
|
||||
<AlertIcon boxSize="40px" mr={0} />
|
||||
<AlertTitle mt={4} mb={1} fontSize="lg">
|
||||
No channels
|
||||
</AlertTitle>
|
||||
<AlertDescription maxWidth="sm">Looks like you have not joined any channels.</AlertDescription>
|
||||
<Button as={RouterLink} to="explore" variant="link" p="2">
|
||||
Explore
|
||||
</Button>
|
||||
</Alert>
|
||||
)}
|
||||
</ContainedParentView>
|
||||
);
|
||||
}
|
||||
|
@ -3,8 +3,15 @@ import { RouteObject } from "react-router-dom";
|
||||
|
||||
const ChannelsHomeView = lazy(() => import("."));
|
||||
const ChannelView = lazy(() => import("./channel"));
|
||||
const ChannelsExploreView = lazy(() => import("./explore"));
|
||||
|
||||
export default [
|
||||
{ index: true, Component: ChannelsHomeView },
|
||||
{ path: ":id", Component: ChannelView },
|
||||
{
|
||||
Component: ChannelsHomeView,
|
||||
children: [
|
||||
{ index: true, Component: ChannelsExploreView },
|
||||
{ path: "explore", Component: ChannelsExploreView },
|
||||
{ path: ":id", Component: ChannelView },
|
||||
],
|
||||
},
|
||||
] satisfies RouteObject[];
|
||||
|
@ -4,7 +4,6 @@ import { UNSAFE_DataRouterContext, useLocation, useNavigate } from "react-router
|
||||
import { NostrEvent, kinds } from "nostr-tools";
|
||||
|
||||
import { ThreadIcon } from "../../components/icons";
|
||||
import UserAvatar from "../../components/user/user-avatar";
|
||||
import UserLink from "../../components/user/user-link";
|
||||
import RequireCurrentAccount from "../../components/router/require-current-account";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
@ -24,9 +23,10 @@ import useAppSettings from "../../hooks/use-user-app-settings";
|
||||
import { truncateId } from "../../helpers/string";
|
||||
import useRouterMarker from "../../hooks/use-router-marker";
|
||||
import decryptionCacheService from "../../services/decryption-cache";
|
||||
import SimpleView from "../../components/layout/presets/simple-view";
|
||||
import UserDnsIdentityIcon from "../../components/user/user-dns-identity-icon";
|
||||
import SimpleHeader from "../../components/layout/presets/simple-header";
|
||||
import UserAvatarLink from "../../components/user/user-avatar-link";
|
||||
import ContainedSimpleView from "../../components/layout/presets/contained-simple-view";
|
||||
|
||||
/** This is broken out from DirectMessageChatPage for performance reasons. Don't use outside of file */
|
||||
const ChatLog = memo(({ messages }: { messages: NostrEvent[] }) => {
|
||||
@ -114,43 +114,40 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
|
||||
|
||||
return (
|
||||
<ThreadsProvider timeline={loader}>
|
||||
<Flex flex={1} direction="column" pr="var(--safe-right)" pl="var(--safe-left)" h="full" overflow="hidden">
|
||||
<SimpleHeader
|
||||
position="initial"
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<ContainedSimpleView
|
||||
reverse
|
||||
title={
|
||||
<Flex gap="2" alignItems="center">
|
||||
<UserAvatar pubkey={pubkey} size="sm" />
|
||||
<UserAvatarLink pubkey={pubkey} size="sm" />
|
||||
<UserLink pubkey={pubkey} fontWeight="bold" />
|
||||
<UserDnsIdentityIcon pubkey={pubkey} />
|
||||
</Flex>
|
||||
}
|
||||
actions={
|
||||
<ButtonGroup ml="auto">
|
||||
{!autoDecryptDMs && (
|
||||
<Button onClick={decryptAll} isLoading={loading}>
|
||||
Decrypt All
|
||||
</Button>
|
||||
)}
|
||||
<IconButton
|
||||
aria-label="Threads"
|
||||
title="Threads"
|
||||
icon={<ThreadIcon boxSize={5} />}
|
||||
onClick={openDrawerList}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
}
|
||||
bottom={<SendMessageForm flexShrink={0} pubkey={pubkey} p="2" />}
|
||||
>
|
||||
<ButtonGroup ml="auto">
|
||||
{!autoDecryptDMs && (
|
||||
<Button onClick={decryptAll} isLoading={loading}>
|
||||
Decrypt All
|
||||
</Button>
|
||||
)}
|
||||
<IconButton
|
||||
aria-label="Threads"
|
||||
title="Threads"
|
||||
icon={<ThreadIcon boxSize={5} />}
|
||||
onClick={openDrawerList}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</SimpleHeader>
|
||||
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Flex h="0" flex={1} overflowX="hidden" overflowY="auto" direction="column-reverse" gap="2" py="4" px="2">
|
||||
<ChatLog messages={messages} />
|
||||
<TimelineActionAndStatus timeline={loader} />
|
||||
</Flex>
|
||||
<SendMessageForm flexShrink={0} pubkey={pubkey} p="2" />
|
||||
<ChatLog messages={messages} />
|
||||
<TimelineActionAndStatus timeline={loader} />
|
||||
{location.state?.thread && (
|
||||
<ThreadDrawer isOpen onClose={closeDrawer} threadId={location.state.thread} pubkey={pubkey} />
|
||||
)}
|
||||
</IntersectionObserverProvider>
|
||||
</Flex>
|
||||
</ContainedSimpleView>
|
||||
</IntersectionObserverProvider>
|
||||
</ThreadsProvider>
|
||||
);
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { useForm } from "react-hook-form";
|
||||
import dayjs from "dayjs";
|
||||
import { kinds } from "nostr-tools";
|
||||
|
||||
import { Button, Flex, FlexProps, Heading } from "@chakra-ui/react";
|
||||
import { Button, ButtonGroup, Flex, FlexProps, Heading } from "@chakra-ui/react";
|
||||
import { useSigningContext } from "../../../providers/global/signing-provider";
|
||||
import MagicTextArea, { RefType } from "../../../components/magic-textarea";
|
||||
import useTextAreaUploadFile, { useTextAreaInsertTextWithForm } from "../../../hooks/use-textarea-upload-file";
|
||||
@ -13,6 +13,7 @@ import { usePublishEvent } from "../../../providers/global/publish-provider";
|
||||
import useCacheForm from "../../../hooks/use-cache-form";
|
||||
import decryptionCacheService from "../../../services/decryption-cache";
|
||||
import InsertGifButton from "../../../components/gif/insert-gif-button";
|
||||
import InsertReactionButton from "../../../components/reactions/insert-reaction-button";
|
||||
|
||||
export default function SendMessageForm({
|
||||
pubkey,
|
||||
@ -99,8 +100,13 @@ export default function SendMessageForm({
|
||||
}}
|
||||
/>
|
||||
<Flex gap="2" direction="column">
|
||||
<Button type="submit">Send</Button>
|
||||
<InsertGifButton onSelectURL={insertText} aria-label="Add gif" />
|
||||
<ButtonGroup size="sm">
|
||||
<InsertGifButton onSelectURL={insertText} aria-label="Add gif" />
|
||||
<InsertReactionButton onSelect={insertText} aria-label="Add emoji" />
|
||||
</ButtonGroup>
|
||||
<Button type="submit" colorScheme="primary">
|
||||
Send
|
||||
</Button>
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
|
@ -114,6 +114,8 @@ export default function UserAboutTab() {
|
||||
pt={metadata?.banner ? 0 : "2"}
|
||||
pb="8"
|
||||
minH="90vh"
|
||||
w="full"
|
||||
flex={1}
|
||||
>
|
||||
<Box
|
||||
pt={!expanded.isOpen ? "20vh" : 0}
|
||||
|
@ -6,10 +6,10 @@ import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import ArticleCard from "../articles/components/article-card";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import useMaxPageWidth from "../../hooks/use-max-page-width";
|
||||
import SimpleView from "../../components/layout/presets/simple-view";
|
||||
|
||||
export default function UserArticlesTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
@ -24,15 +24,15 @@ export default function UserArticlesTab() {
|
||||
const maxWidth = useMaxPageWidth();
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<VerticalPageLayout maxW={maxWidth} mx="auto">
|
||||
<SimpleView maxW={maxWidth} title="Articles" center>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{articles?.map((article) => (
|
||||
<ErrorBoundary key={article.id} event={article}>
|
||||
<ArticleCard article={article} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
<TimelineActionAndStatus timeline={loader} />
|
||||
</VerticalPageLayout>
|
||||
</IntersectionObserverProvider>
|
||||
</IntersectionObserverProvider>
|
||||
</SimpleView>
|
||||
);
|
||||
}
|
||||
|
@ -10,18 +10,7 @@ export type UserCardProps = { pubkey: string; relay?: string } & Omit<FlexProps,
|
||||
|
||||
export const UserCard = memo(({ pubkey, relay, ...props }: UserCardProps) => {
|
||||
return (
|
||||
<Flex
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
pl="3"
|
||||
pr="3"
|
||||
pt="2"
|
||||
pb="2"
|
||||
overflow="hidden"
|
||||
gap="4"
|
||||
alignItems="center"
|
||||
{...props}
|
||||
>
|
||||
<Flex p="1" overflow="hidden" gap="4" alignItems="center" {...props}>
|
||||
<UserAvatarLink pubkey={pubkey} />
|
||||
<Flex direction="column" flex={1} overflow="hidden">
|
||||
<UserLink pubkey={pubkey} fontWeight="bold" />
|
||||
|
@ -12,6 +12,7 @@ import { getPackCordsFromFavorites } from "../../helpers/nostr/emoji-packs";
|
||||
import useFavoriteEmojiPacks from "../../hooks/use-favorite-emoji-packs";
|
||||
import useReplaceableEvents from "../../hooks/use-replaceable-events";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import SimpleView from "../../components/layout/presets/simple-view";
|
||||
|
||||
export default function UserEmojiPacksTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
@ -28,8 +29,8 @@ export default function UserEmojiPacksTab() {
|
||||
const callback = useTimelineCurserIntersectionCallback(loader);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<VerticalPageLayout>
|
||||
<SimpleView title="Emojis">
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{packs.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg" mt="2">
|
||||
@ -54,7 +55,7 @@ export default function UserEmojiPacksTab() {
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
</VerticalPageLayout>
|
||||
</IntersectionObserverProvider>
|
||||
</IntersectionObserverProvider>
|
||||
</SimpleView>
|
||||
);
|
||||
}
|
||||
|
@ -7,13 +7,13 @@ import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import Timestamp from "../../components/timestamp";
|
||||
import useEventIntersectionRef from "../../hooks/use-event-intersection-ref";
|
||||
import { formatBytes } from "../../helpers/number";
|
||||
import useShareableEventAddress from "../../hooks/use-shareable-event-address";
|
||||
import SimpleView from "../../components/layout/presets/simple-view";
|
||||
|
||||
function FileRow({ file }: { file: NostrEvent }) {
|
||||
const ref = useEventIntersectionRef<HTMLTableRowElement>(file);
|
||||
@ -52,8 +52,8 @@ export default function UserFilesTab() {
|
||||
const callback = useTimelineCurserIntersectionCallback(loader);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<VerticalPageLayout>
|
||||
<SimpleView title="Files">
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<TableContainer>
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
@ -72,7 +72,7 @@ export default function UserFilesTab() {
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<TimelineActionAndStatus timeline={loader} />
|
||||
</VerticalPageLayout>
|
||||
</IntersectionObserverProvider>
|
||||
</IntersectionObserverProvider>
|
||||
</SimpleView>
|
||||
);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import useUserContactList from "../../hooks/use-user-contact-list";
|
||||
import { getPubkeysFromList } from "../../helpers/nostr/lists";
|
||||
import { useWebOfTrust } from "../../providers/global/web-of-trust-provider";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import SimpleView from "../../components/layout/presets/simple-view";
|
||||
|
||||
export default function UserFollowingTab() {
|
||||
const webOfTrust = useWebOfTrust();
|
||||
@ -21,12 +22,14 @@ export default function UserFollowingTab() {
|
||||
if (!contactsList) return <Spinner />;
|
||||
|
||||
return (
|
||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2" p="2">
|
||||
{sorted.map(({ pubkey, relay }) => (
|
||||
<ErrorBoundary key={pubkey}>
|
||||
<UserCard pubkey={pubkey} relay={relay} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<SimpleView title="Following">
|
||||
<SimpleGrid columns={{ base: 1, lg: 2, xl: 3 }} spacing="2" p="2">
|
||||
{sorted.map(({ pubkey, relay }) => (
|
||||
<ErrorBoundary key={pubkey}>
|
||||
<UserCard pubkey={pubkey} relay={relay} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</SimpleView>
|
||||
);
|
||||
}
|
||||
|
@ -1,32 +1,3 @@
|
||||
import { Suspense, useState } from "react";
|
||||
import {
|
||||
Flex,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
List,
|
||||
ListItem,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
NumberDecrementStepper,
|
||||
NumberIncrementStepper,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
Spinner,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Tabs,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { Outlet, useMatches, useNavigate } from "react-router-dom";
|
||||
|
||||
import useUserProfile from "../../hooks/use-user-profile";
|
||||
import { getDisplayName } from "../../helpers/nostr/profile";
|
||||
import { useAppTitle } from "../../hooks/use-app-title";
|
||||
@ -34,11 +5,17 @@ import { useReadRelays } from "../../hooks/use-client-relays";
|
||||
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||
import { AdditionalRelayProvider } from "../../providers/local/additional-relay-context";
|
||||
import { unique } from "../../helpers/array";
|
||||
import { RelayFavicon } from "../../components/relay-favicon";
|
||||
import Header from "./components/header";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import useParamsProfilePointer from "../../hooks/use-params-pubkey-pointer";
|
||||
import useUserMailboxes from "../../hooks/use-user-mailboxes";
|
||||
import SimpleParentView from "../../components/layout/presets/simple-parent-view";
|
||||
import SimpleNavItem from "../../components/layout/presets/simple-nav-item";
|
||||
import { Box, Flex, Heading, IconButton } from "@chakra-ui/react";
|
||||
import UserAvatar from "../../components/user/user-avatar";
|
||||
import { DirectMessagesIcon } from "../../components/icons";
|
||||
import RouterLink from "../../components/router-link";
|
||||
import UserName from "../../components/user/user-name";
|
||||
import UserDnsIdentity from "../../components/user/user-dns-identity";
|
||||
import UserAboutContent from "../../components/user/user-about-content";
|
||||
|
||||
const tabs = [
|
||||
{ label: "About", path: "about" },
|
||||
@ -71,87 +48,53 @@ function useUserBestOutbox(pubkey: string, count: number = 4) {
|
||||
|
||||
export default function UserView() {
|
||||
const { pubkey, relays: pointerRelays = [] } = useParamsProfilePointer();
|
||||
const navigate = useNavigate();
|
||||
const [relayCount, setRelayCount] = useState(4);
|
||||
const userTopRelays = useUserBestOutbox(pubkey, relayCount);
|
||||
const relayModal = useDisclosure();
|
||||
const userTopRelays = useUserBestOutbox(pubkey, 4);
|
||||
const readRelays = unique([...userTopRelays, ...pointerRelays]);
|
||||
|
||||
const metadata = useUserProfile(pubkey, userTopRelays, true);
|
||||
useAppTitle(getDisplayName(metadata, pubkey));
|
||||
|
||||
const matches = useMatches();
|
||||
const lastMatch = matches[matches.length - 1];
|
||||
|
||||
const activeTab = tabs.indexOf(tabs.find((t) => lastMatch.pathname.endsWith(t.path)) ?? tabs[0]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AdditionalRelayProvider relays={readRelays}>
|
||||
<Flex direction="column" alignItems="stretch" gap="2">
|
||||
<Header pubkey={pubkey} showRelaySelectionModal={relayModal.onOpen} />
|
||||
<Tabs
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
flexGrow="1"
|
||||
isLazy
|
||||
index={activeTab}
|
||||
onChange={(v) => navigate(tabs[v].path, { replace: true })}
|
||||
colorScheme="primary"
|
||||
h="full"
|
||||
>
|
||||
<TabList overflowX="auto" overflowY="hidden" flexShrink={0}>
|
||||
{tabs.map(({ label }) => (
|
||||
<Tab key={label} whiteSpace="pre">
|
||||
{label}
|
||||
</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
|
||||
<TabPanels>
|
||||
{tabs.map(({ label }) => (
|
||||
<TabPanel key={label} p={0}>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Outlet context={{ pubkey, setRelayCount }} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<AdditionalRelayProvider relays={readRelays}>
|
||||
<SimpleParentView path="/u/:pubkey" context={{ pubkey }}>
|
||||
<Flex
|
||||
direction="column"
|
||||
gap="2"
|
||||
p="4"
|
||||
pt="max(1rem, var(--safe-top))"
|
||||
backgroundImage={metadata?.banner && `url(${metadata?.banner})`}
|
||||
backgroundPosition="center"
|
||||
backgroundRepeat="no-repeat"
|
||||
backgroundSize="cover"
|
||||
position="relative"
|
||||
rounded="md"
|
||||
>
|
||||
<UserAvatar pubkey={pubkey} size="xl" float="left" />
|
||||
{/* <IconButton
|
||||
icon={<DirectMessagesIcon boxSize={5} />}
|
||||
as={RouterLink}
|
||||
to={`/messages/${pubkey}`}
|
||||
aria-label="Direct Message"
|
||||
colorScheme="blue"
|
||||
rounded="full"
|
||||
position="absolute"
|
||||
bottom="-6"
|
||||
right="4"
|
||||
size="lg"
|
||||
/> */}
|
||||
</Flex>
|
||||
</AdditionalRelayProvider>
|
||||
|
||||
<Modal isOpen={relayModal.isOpen} onClose={relayModal.onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader pb="1">Relay selection</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<List spacing="2">
|
||||
{userTopRelays.map((url) => (
|
||||
<ListItem key={url}>
|
||||
<RelayFavicon relay={url} size="xs" mr="2" />
|
||||
{url}
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<FormControl>
|
||||
<FormLabel>Max relays</FormLabel>
|
||||
<NumberInput min={0} step={1} value={relayCount} onChange={(v) => setRelayCount(parseInt(v) || 0)}>
|
||||
<NumberInputField />
|
||||
<NumberInputStepper>
|
||||
<NumberIncrementStepper />
|
||||
<NumberDecrementStepper />
|
||||
</NumberInputStepper>
|
||||
</NumberInput>
|
||||
<FormHelperText>set to 0 to connect to all relays</FormHelperText>
|
||||
</FormControl>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
<Flex direction="column" overflow="hidden">
|
||||
<Heading size="md">
|
||||
<UserName pubkey={pubkey} isTruncated />
|
||||
</Heading>
|
||||
<UserDnsIdentity pubkey={pubkey} fontSize="sm" />
|
||||
</Flex>
|
||||
{tabs.map(({ label, path }) => (
|
||||
<SimpleNavItem key={label} to={`./${path}`}>
|
||||
{label}
|
||||
</SimpleNavItem>
|
||||
))}
|
||||
</SimpleParentView>
|
||||
</AdditionalRelayProvider>
|
||||
);
|
||||
}
|
||||
|
@ -5,8 +5,8 @@ import { kinds } from "nostr-tools";
|
||||
|
||||
import { isJunkList } from "../../helpers/nostr/lists";
|
||||
import ListCard from "../lists/components/list-card";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import useUserSets from "../../hooks/use-user-lists";
|
||||
import SimpleView from "../../components/layout/presets/simple-view";
|
||||
|
||||
export default function UserListsTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
@ -19,7 +19,7 @@ export default function UserListsTab() {
|
||||
const columns = { base: 1, lg: 2, xl: 3, "2xl": 4 };
|
||||
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
<SimpleView title="Lists">
|
||||
<Heading size="md" mt="2">
|
||||
Special lists
|
||||
</Heading>
|
||||
@ -70,6 +70,6 @@ export default function UserListsTab() {
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)}
|
||||
</VerticalPageLayout>
|
||||
</SimpleView>
|
||||
);
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import { RelayShareButton } from "../relays/components/relay-share-button";
|
||||
import useUserMailboxes from "../../hooks/use-user-mailboxes";
|
||||
import { truncateId } from "../../helpers/string";
|
||||
import SimpleView from "../../components/layout/presets/simple-view";
|
||||
|
||||
function Relay({ url, reviews }: { url: string; reviews: NostrEvent[] }) {
|
||||
const { info } = useRelayInfo(url);
|
||||
@ -73,40 +74,42 @@ const UserRelaysTab = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Heading size="lg" ml="2" mt="2">
|
||||
Inboxes
|
||||
</Heading>
|
||||
<VStack divider={<StackDivider />} py="2" align="stretch">
|
||||
{Array.from(mailboxes?.inboxes ?? []).map((url) => (
|
||||
<ErrorBoundary key={url}>
|
||||
<Relay url={url} reviews={getRelayReviews(url, reviews)} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</VStack>
|
||||
<Heading size="lg" ml="2" mt="2">
|
||||
Outboxes
|
||||
</Heading>
|
||||
<VStack divider={<StackDivider />} py="2" align="stretch">
|
||||
{Array.from(mailboxes?.outboxes ?? []).map((url) => (
|
||||
<ErrorBoundary key={url}>
|
||||
<Relay url={url} reviews={getRelayReviews(url, reviews)} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</VStack>
|
||||
{otherReviews.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg" ml="2" mt="2">
|
||||
Reviews
|
||||
</Heading>
|
||||
<Flex direction="column" gap="2" pb="8">
|
||||
{otherReviews.map((event) => (
|
||||
<RelayReviewNote key={event.id} event={event} />
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</IntersectionObserverProvider>
|
||||
<SimpleView title="Relays">
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<Heading size="lg" ml="2" mt="2">
|
||||
Inboxes
|
||||
</Heading>
|
||||
<VStack divider={<StackDivider />} py="2" align="stretch">
|
||||
{Array.from(mailboxes?.inboxes ?? []).map((url) => (
|
||||
<ErrorBoundary key={url}>
|
||||
<Relay url={url} reviews={getRelayReviews(url, reviews)} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</VStack>
|
||||
<Heading size="lg" ml="2" mt="2">
|
||||
Outboxes
|
||||
</Heading>
|
||||
<VStack divider={<StackDivider />} py="2" align="stretch">
|
||||
{Array.from(mailboxes?.outboxes ?? []).map((url) => (
|
||||
<ErrorBoundary key={url}>
|
||||
<Relay url={url} reviews={getRelayReviews(url, reviews)} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</VStack>
|
||||
{otherReviews.length > 0 && (
|
||||
<>
|
||||
<Heading size="lg" ml="2" mt="2">
|
||||
Reviews
|
||||
</Heading>
|
||||
<Flex direction="column" gap="2" pb="8">
|
||||
{otherReviews.map((event) => (
|
||||
<RelayReviewNote key={event.id} event={event} />
|
||||
))}
|
||||
</Flex>
|
||||
</>
|
||||
)}
|
||||
</IntersectionObserverProvider>
|
||||
</SimpleView>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -10,7 +10,7 @@ import IntersectionObserverProvider from "../../providers/local/intersection-obs
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import StreamCard from "../streams/components/stream-card";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import SimpleView from "../../components/layout/presets/simple-view";
|
||||
|
||||
export default function UserStreamsTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
@ -27,15 +27,15 @@ export default function UserStreamsTab() {
|
||||
const callback = useTimelineCurserIntersectionCallback(loader);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<VerticalPageLayout>
|
||||
<SimpleView title="Streams">
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<SimpleGrid columns={{ base: 1, md: 2, lg: 3, xl: 4, "2xl": 5 }} spacing="2">
|
||||
{streams.map((stream) => (
|
||||
<StreamCard key={getEventUID(stream)} stream={stream} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
<TimelineActionAndStatus timeline={loader} />
|
||||
</VerticalPageLayout>
|
||||
</IntersectionObserverProvider>
|
||||
</IntersectionObserverProvider>
|
||||
</SimpleView>
|
||||
);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import { TORRENT_KIND, validateTorrent } from "../../helpers/nostr/torrents";
|
||||
import TorrentTableRow from "../torrents/components/torrent-table-row";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import SimpleView from "../../components/layout/presets/simple-view";
|
||||
|
||||
export default function UserTorrentsTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
@ -29,8 +30,8 @@ export default function UserTorrentsTab() {
|
||||
const callback = useTimelineCurserIntersectionCallback(loader);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<VerticalPageLayout>
|
||||
<SimpleView title="Torrents">
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<TableContainer>
|
||||
<Table size="sm">
|
||||
<Thead>
|
||||
@ -48,7 +49,7 @@ export default function UserTorrentsTab() {
|
||||
</TableContainer>
|
||||
|
||||
<TimelineActionAndStatus timeline={loader} />
|
||||
</VerticalPageLayout>
|
||||
</IntersectionObserverProvider>
|
||||
</IntersectionObserverProvider>
|
||||
</SimpleView>
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user