Add Streams and Tools to launchpad

This commit is contained in:
hzrd149 2024-09-02 08:54:06 -05:00
parent d5aa803eae
commit c648923508
9 changed files with 190 additions and 30 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add Streams and Tools to launchpad

View File

@ -3,9 +3,12 @@ import { DraftNostrEvent, NostrEvent, isPTag } from "../../types/nostr-event";
import { unique } from "../array";
import { ensureNotifyContentMentions } from "./post";
import { getEventCoordinate } from "./event";
import { kinds } from "nostr-tools";
export const STREAM_KIND = 30311;
export const STREAM_CHAT_MESSAGE_KIND = 1311;
/** @deprecated use kinds.LiveEvent instead */
export const STREAM_KIND = kinds.LiveEvent;
/** @deprecated use kinds.LiveChatMessage instead */
export const STREAM_CHAT_MESSAGE_KIND = kinds.LiveChatMessage;
export type ParsedStream = {
event: NostrEvent;

View File

@ -105,6 +105,9 @@ export default function DMsCard({ ...props }: Omit<CardProps, "children">) {
{conversations.slice(0, 4).map((conversation) => (
<Conversation key={conversation.pubkeys.join("-")} conversation={conversation} />
))}
<Button as={RouterLink} to="/dm" flexShrink={0} variant="link" size="lg" py="4">
View More
</Button>
</CardBody>
</Card>
);

View File

@ -1,13 +1,13 @@
import { useCallback } from "react";
import { Button, Card, CardBody, CardHeader, CardProps, Heading, Link } from "@chakra-ui/react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { NostrEvent } from "nostr-tools";
import KeyboardShortcut from "../../../components/keyboard-shortcut";
import { useNotifications } from "../../../providers/global/notifications-provider";
import useSubject from "../../../hooks/use-subject";
import { NotificationType, typeSymbol } from "../../../classes/notifications";
import NotificationItem from "../../notifications/components/notification-item";
import { useCallback } from "react";
import { NostrEvent } from "nostr-tools";
export default function NotificationsCard({ ...props }: Omit<CardProps, "children">) {
const navigate = useNavigate();
@ -40,11 +40,11 @@ export default function NotificationsCard({ ...props }: Omit<CardProps, "childre
</Heading>
<KeyboardShortcut letter="i" requireMeta ml="auto" onPress={() => navigate("/notifications")} />
</CardHeader>
<CardBody overflowX="hidden" overflowY="auto" pt="4" display="flex" gap="2" flexDirection="column" maxH="50vh">
<CardBody overflowX="hidden" overflowY="auto" pt="4" display="flex" gap="2" flexDirection="column">
{limit.map((event) => (
<NotificationItem event={event} key={event.id} onClick={handleClick} />
))}
<Button as={RouterLink} to="/notifications" flexShrink={0} variant="link" size="lg" py="6">
<Button as={RouterLink} to="/notifications" flexShrink={0} variant="link" size="lg" py="4">
View More
</Button>
</CardBody>

View File

@ -0,0 +1,89 @@
import { useCallback, useMemo } from "react";
import { Button, Card, CardBody, CardHeader, CardProps, Flex, Heading, Link, LinkBox } from "@chakra-ui/react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { Filter, kinds, NostrEvent } from "nostr-tools";
import { getEventUID } from "nostr-idb";
import { useReadRelays } from "../../../hooks/use-client-relays";
import useClientSideMuteFilter from "../../../hooks/use-client-side-mute-filter";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import PeopleListProvider, { usePeopleListContext } from "../../../providers/local/people-list-provider";
import useSubject from "../../../hooks/use-subject";
import { ParsedStream, parseStreamEvent } from "../../../helpers/nostr/stream";
import UserAvatar from "../../../components/user/user-avatar";
import UserName from "../../../components/user/user-name";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
import useShareableEventAddress from "../../../hooks/use-shareable-event-address";
import KeyboardShortcut from "../../../components/keyboard-shortcut";
function LiveStream({ stream }: { stream: ParsedStream }) {
const naddr = useShareableEventAddress(stream.event);
return (
<Flex as={LinkBox} alignItems="center" gap="2">
<UserAvatar pubkey={stream.host} size="sm" />
<HoverLinkOverlay as={RouterLink} to={`/streams/${naddr}`}></HoverLinkOverlay>
{stream.title || <UserName pubkey={stream.host} />}
</Flex>
);
}
function StreamsCardContent({ ...props }: Omit<CardProps, "children">) {
const navigate = useNavigate();
const relays = useReadRelays();
const userMuteFilter = useClientSideMuteFilter();
const eventFilter = useCallback(
(event: NostrEvent) => {
if (userMuteFilter(event)) return false;
return true;
},
[userMuteFilter],
);
const { filter, listId } = usePeopleListContext();
const query = useMemo<Filter[] | undefined>(() => {
if (!filter) return undefined;
return [
{ authors: filter.authors, kinds: [kinds.LiveEvent] },
{ "#p": filter.authors, kinds: [kinds.LiveEvent] },
];
}, [filter]);
const timeline = useTimelineLoader(`${listId ?? "global"}-streams`, relays, query, { eventFilter });
const streams = useSubject(timeline.timeline)
.map((event) => parseStreamEvent(event))
.filter((stream) => !!stream.streaming)
.slice(0, 6);
return (
<Card variant="outline" {...props}>
<CardHeader display="flex" justifyContent="space-between" alignItems="center" pb="2">
<Heading size="lg">
<Link as={RouterLink} to="/streams">
Streams
</Link>
</Heading>
<KeyboardShortcut letter="l" requireMeta ml="auto" onPress={() => navigate("/streams")} />
</CardHeader>
<CardBody overflowX="hidden" overflowY="auto" pt="4" display="flex" gap="2" flexDirection="column" maxH="50vh">
{streams.map((stream) => (
<LiveStream key={getEventUID(stream.event)} stream={stream} />
))}
<Button as={RouterLink} to="/streams" flexShrink={0} variant="link" size="lg" py="4">
View More
</Button>
</CardBody>
</Card>
);
}
export default function StreamsCard({ ...props }: Omit<CardProps, "children">) {
return (
<PeopleListProvider initList="following">
<StreamsCardContent {...props} />
</PeopleListProvider>
);
}

View File

@ -0,0 +1,57 @@
import { useState } from "react";
import { Button, Card, CardBody, CardHeader, CardProps, Heading, Input, Link, SimpleGrid } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import useRecentIds from "../../../hooks/use-recent-ids";
import { allApps } from "../../other-stuff/apps";
import AppCard from "../../other-stuff/component/app-card";
export default function ToolsCard({ ...props }: Omit<CardProps, "children">) {
const { recent: recentApps, useThing: useApp } = useRecentIds("apps");
const [search, setSearch] = useState("");
const apps = Array(6)
.fill(0)
.map((_, i) => {
if (recentApps[i]) {
return allApps.find((a) => a.id === recentApps[i]) || allApps[i];
} else return allApps[i];
});
return (
<Card variant="outline" {...props}>
<CardHeader display="flex" justifyContent="space-between" alignItems="center" pb="2">
<Heading size="lg">
<Link as={RouterLink} to="/other-stuff">
Tools
</Link>
</Heading>
<Input
type="search"
placeholder="Search apps"
maxW="sm"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</CardHeader>
<CardBody overflowX="hidden" overflowY="auto" pt="4" display="flex" gap="2" flexDirection="column" maxH="50vh">
<SimpleGrid spacing="2" columns={{ base: 1, md: 2 }}>
{search.length > 2
? allApps
.filter(
(app) =>
app.title.toLowerCase().includes(search.toLowerCase()) ||
app.description.toLowerCase().includes(search.toLowerCase()),
)
.map((app) => <AppCard key={app.id} app={app} onClick={() => useApp(app.id)} />)
: apps.map((app) => <AppCard key={app.id} app={app} onClick={() => useApp(app.id)} />)}
</SimpleGrid>
<Button as={RouterLink} to="/other-stuff" flexShrink={0} variant="link" size="lg" py="4">
View More
</Button>
</CardBody>
</Card>
);
}

View File

@ -13,6 +13,8 @@ import SearchForm from "./components/search-form";
import KeyboardShortcut from "../../components/keyboard-shortcut";
import DMsCard from "./components/dms-card";
import NotificationsCard from "./components/notifications-card";
import ToolsCard from "./components/tools-card";
import StreamsCard from "./components/streams-card";
function LaunchpadPage() {
const { openModal } = useContext(PostModalContext);
@ -39,8 +41,10 @@ function LaunchpadPage() {
<SearchForm flex={1} />
</Flex>
<FeedsCard w="full" />
<NotificationsCard w={{ base: "full", md: "calc(60% - 1rem)" }} />
<NotificationsCard w={{ base: "full", md: "calc(60% - 1rem)" }} maxH="40vh" />
<DMsCard w={{ base: "full", md: "40%" }} />
<StreamsCard w={{ base: "full", md: "40%" }} />
<ToolsCard w={{ base: "full", md: "calc(60% - 1rem)" }} />
</VerticalPageLayout>
);
}

View File

@ -57,6 +57,27 @@ export const internalApps: App[] = [
];
export const internalTools: App[] = [
{
title: "Event Console",
description: "Find events based on nostr filters",
icon: SearchIcon,
id: "console",
to: "/tools/console",
},
{
title: "Event Publisher",
description: "Write and publish events",
icon: UploadCloud01,
id: "publisher",
to: "/tools/publisher ",
},
{
title: "Unknown Events",
description: "A timeline of unknown events",
icon: MessageQuestionSquare,
id: "unknown",
to: "/tools/unknown",
},
{
title: "Satellite CDN",
description: "Scalable media hosting for the nostr ecosystem",
@ -93,27 +114,6 @@ export const internalTools: App[] = [
id: "dm-timeline",
to: "/tools/dm-timeline",
},
{
title: "Unknown Events",
description: "A timeline of unknown events",
icon: MessageQuestionSquare,
id: "unknown",
to: "/tools/unknown",
},
{
title: "Event Console",
description: "Find events based on nostr filters",
icon: SearchIcon,
id: "console",
to: "/tools/console",
},
{
title: "Event Publisher",
description: "Write and publish events",
icon: UploadCloud01,
id: "publisher",
to: "/tools/publisher ",
},
{
title: "Corrections Feed",
description: "A feed of post edits",

View File

@ -8,7 +8,6 @@ import { useAdditionalRelayContext } from "../../providers/local/additional-rela
import { RelayIconStack } from "../../components/relay-icon-stack";
import { NostrEvent } from "../../types/nostr-event";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { STREAM_KIND } from "../../helpers/nostr/stream";
import TimelineViewType from "../../components/timeline-page/timeline-view-type";
import TimelinePage, { useTimelinePageEventFilter } from "../../components/timeline-page";
import NoteFilterTypeButtons from "../../components/note-filter-type-buttons";
@ -35,7 +34,7 @@ export default function UserNotesTab() {
readRelays,
{
authors: [pubkey],
kinds: [kinds.ShortTextNote, kinds.Repost, kinds.GenericRepost, kinds.LongFormArticle, STREAM_KIND, 2],
kinds: [kinds.ShortTextNote, kinds.Repost, kinds.GenericRepost, 2],
},
{ eventFilter },
);