mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-01 00:19:45 +02:00
Add track view for stemstr tracks
This commit is contained in:
parent
56fc645f16
commit
d1af1e1271
5
.changeset/green-jobs-kiss.md
Normal file
5
.changeset/green-jobs-kiss.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add track view for stemstr tracks
|
@ -80,6 +80,7 @@ import SatelliteCDNView from "./views/tools/satellite-cdn";
|
||||
import OtherStuffView from "./views/other-stuff";
|
||||
import { RouteProviders } from "./providers/route";
|
||||
import LaunchpadView from "./views/launchpad";
|
||||
import TracksView from "./views/tracks";
|
||||
const UserTracksTab = lazy(() => import("./views/user/tracks"));
|
||||
|
||||
const ToolsHomeView = lazy(() => import("./views/tools"));
|
||||
@ -366,6 +367,10 @@ const router = createHashRouter([
|
||||
path: "streams",
|
||||
element: <StreamsView />,
|
||||
},
|
||||
{
|
||||
path: "tracks",
|
||||
element: <TracksView />,
|
||||
},
|
||||
{ path: "l/:link", element: <NostrLinkView /> },
|
||||
{ path: "t/:hashtag", element: <HashTagView /> },
|
||||
{
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { ReactNode } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
@ -9,9 +7,6 @@ import {
|
||||
CardHeader,
|
||||
CardProps,
|
||||
Flex,
|
||||
IconButton,
|
||||
Image,
|
||||
Link,
|
||||
Tag,
|
||||
Tooltip,
|
||||
} from "@chakra-ui/react";
|
||||
@ -20,29 +15,17 @@ import { NostrEvent } from "../../../types/nostr-event";
|
||||
import UserAvatarLink from "../../user-avatar-link";
|
||||
import UserLink from "../../user-link";
|
||||
import { CompactNoteContent } from "../../compact-note-content";
|
||||
import { getDownloadURL, getHashtags, getStreamURL } from "../../../helpers/nostr/stemstr";
|
||||
import { DownloadIcon, ReplyIcon } from "../../icons";
|
||||
import { getHashtags } from "../../../helpers/nostr/stemstr";
|
||||
import { ReplyIcon } from "../../icons";
|
||||
import NoteZapButton from "../../note/note-zap-button";
|
||||
import QuoteRepostButton from "../../note/components/quote-repost-button";
|
||||
import Timestamp from "../../timestamp";
|
||||
import { LiveAudioPlayer } from "../../live-audio-player";
|
||||
import TrackStemstrButton from "../../../views/tracks/components/track-stemstr-button";
|
||||
import TrackDownloadButton from "../../../views/tracks/components/track-download-button";
|
||||
import TrackPlayer from "../../../views/tracks/components/track-player";
|
||||
|
||||
// example nevent1qqst32cnyhhs7jt578u7vp3y047dduuwjquztpvwqc43f3nvg8dh28gpzamhxue69uhhyetvv9ujuum5v4khxarj9eshquq4rxdxa
|
||||
export default function EmbeddedStemstrTrack({ track, ...props }: Omit<CardProps, "children"> & { track: NostrEvent }) {
|
||||
const streamUrl = getStreamURL(track);
|
||||
const downloadUrl = getDownloadURL(track);
|
||||
|
||||
let player: ReactNode | null = null;
|
||||
if (streamUrl) {
|
||||
player = <LiveAudioPlayer stream={streamUrl.url} w="full" />;
|
||||
} else if (downloadUrl) {
|
||||
player = (
|
||||
<Box as="audio" controls w="full">
|
||||
<source src={downloadUrl.url} type={downloadUrl.format} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const hashtags = getHashtags(track);
|
||||
|
||||
return (
|
||||
@ -53,7 +36,7 @@ export default function EmbeddedStemstrTrack({ track, ...props }: Omit<CardProps
|
||||
<Timestamp ml="auto" timestamp={track.created_at} />
|
||||
</CardHeader>
|
||||
<CardBody p="2" display="flex" gap="2" flexDirection="column">
|
||||
{player}
|
||||
<TrackPlayer track={track} />
|
||||
<CompactNoteContent event={track} />
|
||||
{hashtags.length > 0 && (
|
||||
<Flex wrap="wrap" gap="2">
|
||||
@ -74,26 +57,8 @@ export default function EmbeddedStemstrTrack({ track, ...props }: Omit<CardProps
|
||||
<NoteZapButton event={track} />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup size="sm" ml="auto">
|
||||
{downloadUrl && (
|
||||
<IconButton
|
||||
as={Link}
|
||||
icon={<DownloadIcon />}
|
||||
aria-label="Download"
|
||||
title="Download"
|
||||
href={downloadUrl.url}
|
||||
download
|
||||
isExternal
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
as={Link}
|
||||
leftIcon={<Image src="https://stemstr.app/favicon.svg" />}
|
||||
href={`https://stemstr.app/thread/${track.id}`}
|
||||
colorScheme="purple"
|
||||
isExternal
|
||||
>
|
||||
View on Stemstr
|
||||
</Button>
|
||||
<TrackDownloadButton track={track} />
|
||||
<TrackStemstrButton track={track} />
|
||||
</ButtonGroup>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
@ -2,6 +2,7 @@ import { CSSProperties } from "react";
|
||||
import { Box, useColorMode } from "@chakra-ui/react";
|
||||
import { EmbedEventPointer } from "../embed-event";
|
||||
import appSettings from "../../services/settings/app-settings";
|
||||
import { STEMSTR_RELAY } from "../../helpers/nostr/stemstr";
|
||||
|
||||
const setZIndex: CSSProperties = { zIndex: 1, position: "relative" };
|
||||
|
||||
@ -121,7 +122,7 @@ export function renderStemstrUrl(match: URL) {
|
||||
const [_, base, id] = match.pathname.split("/");
|
||||
if (base !== "thread" || id.length !== 64) return null;
|
||||
|
||||
return <EmbedEventPointer pointer={{ type: "nevent", data: { id, relays: ["wss://relay.stemstr.app"] } }} />;
|
||||
return <EmbedEventPointer pointer={{ type: "nevent", data: { id, relays: [STEMSTR_RELAY] } }} />;
|
||||
}
|
||||
|
||||
export function renderSoundCloudUrl(match: URL) {
|
||||
|
@ -65,6 +65,7 @@ import Translate01 from "./icons/translate-01";
|
||||
import MessageChatSquare from "./icons/message-chat-square";
|
||||
import Package from "./icons/package";
|
||||
import Magnet from "./icons/magnet";
|
||||
import Recording02 from "./icons/recording-02";
|
||||
|
||||
const defaultProps: IconProps = { boxSize: 4 };
|
||||
|
||||
@ -237,4 +238,5 @@ export const TranslateIcon = Translate01;
|
||||
export const ChannelsIcon = MessageChatSquare;
|
||||
export const ThreadIcon = MessageChatSquare;
|
||||
export const ThingsIcon = Package;
|
||||
export const TorrentIcon = Magnet
|
||||
export const TorrentIcon = Magnet;
|
||||
export const TrackIcon = Recording02;
|
||||
|
@ -79,6 +79,7 @@ export default function NavItems() {
|
||||
else if (location.pathname.startsWith("/settings")) active = "settings";
|
||||
else if (location.pathname.startsWith("/tools")) active = "tools";
|
||||
else if (location.pathname.startsWith("/search")) active = "search";
|
||||
else if (location.pathname.startsWith("/tracks")) active = "tracks";
|
||||
else if (location.pathname.startsWith("/t/")) active = "search";
|
||||
else if (location.pathname.startsWith("/torrents")) active = "tools";
|
||||
else if (location.pathname.startsWith("/map")) active = "tools";
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
export const STEMSTR_RELAY = "wss://relay.stemstr.app";
|
||||
export const STEMSTR_TRACK_KIND = 1808;
|
||||
|
||||
export function getSha256Hash(track: NostrEvent) {
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
MapIcon,
|
||||
MuteIcon,
|
||||
TorrentIcon,
|
||||
TrackIcon,
|
||||
} from "../../components/icons";
|
||||
import { App } from "./component/app-card";
|
||||
import ShieldOff from "../../components/icons/shield-off";
|
||||
@ -35,6 +36,7 @@ export const internalApps: App[] = [
|
||||
{ title: "Torrents", description: "Browse torrents on nostr", icon: TorrentIcon, id: "torrents", to: "/torrents" },
|
||||
{ title: "Emojis", description: "Create custom emoji packs", icon: EmojiPacksIcon, id: "emojis", to: "/emojis" },
|
||||
{ title: "Lists", description: "Browse and create lists", icon: ListsIcon, id: "lists", to: "/lists" },
|
||||
{ title: "Tracks", description: "Browse stemstr tracks", icon: TrackIcon, id: "tracks", to: "/tracks" },
|
||||
// { title: "Things", icon: ThingsIcon, id: "things", to: "/things" },
|
||||
];
|
||||
|
||||
|
62
src/views/tracks/components/track-card.tsx
Normal file
62
src/views/tracks/components/track-card.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useRef } from "react";
|
||||
import { Button, ButtonGroup, Card, CardBody, CardFooter, CardHeader, CardProps, Flex, Tag } from "@chakra-ui/react";
|
||||
|
||||
import { getEventUID } from "../../../helpers/nostr/events";
|
||||
import { useRegisterIntersectionEntity } from "../../../providers/local/intersection-observer";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { getHashtags } from "../../../helpers/nostr/stemstr";
|
||||
import { CompactNoteContent } from "../../../components/compact-note-content";
|
||||
import Timestamp from "../../../components/timestamp";
|
||||
import UserLink from "../../../components/user-link";
|
||||
import UserAvatarLink from "../../../components/user-avatar-link";
|
||||
import { ReplyIcon } from "../../../components/icons";
|
||||
import QuoteRepostButton from "../../../components/note/components/quote-repost-button";
|
||||
import NoteZapButton from "../../../components/note/note-zap-button";
|
||||
import TrackStemstrButton from "./track-stemstr-button";
|
||||
import TrackDownloadButton from "./track-download-button";
|
||||
import TrackPlayer from "./track-player";
|
||||
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
|
||||
import TrackMenu from "./track-menu";
|
||||
|
||||
export default function TrackCard({ track, ...props }: { track: NostrEvent } & Omit<CardProps, "children">) {
|
||||
const hashtags = getHashtags(track);
|
||||
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, getEventUID(track));
|
||||
|
||||
return (
|
||||
<Card variant="outline" ref={ref} {...props}>
|
||||
<CardHeader display="flex" alignItems="center" p="2" pb="0" gap="2">
|
||||
<UserAvatarLink pubkey={track.pubkey} size="sm" />
|
||||
<UserLink pubkey={track.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
|
||||
<UserDnsIdentityIcon pubkey={track.pubkey} onlyIcon />
|
||||
<Timestamp ml="auto" timestamp={track.created_at} />
|
||||
</CardHeader>
|
||||
<CardBody p="2" display="flex" gap="2" flexDirection="column">
|
||||
<TrackPlayer track={track} />
|
||||
<CompactNoteContent event={track} />
|
||||
{hashtags.length > 0 && (
|
||||
<Flex wrap="wrap" gap="2">
|
||||
{hashtags.map((hashtag) => (
|
||||
<Tag key={hashtag}>#{hashtag}</Tag>
|
||||
))}
|
||||
</Flex>
|
||||
)}
|
||||
</CardBody>
|
||||
<CardFooter px="2" pt="0" pb="2" flexWrap="wrap" gap="2">
|
||||
<ButtonGroup size="sm" variant="ghost">
|
||||
<Button leftIcon={<ReplyIcon />} isDisabled>
|
||||
Comment
|
||||
</Button>
|
||||
<QuoteRepostButton event={track} />
|
||||
<NoteZapButton event={track} />
|
||||
</ButtonGroup>
|
||||
<ButtonGroup size="sm" ml="auto">
|
||||
<TrackDownloadButton track={track} />
|
||||
<TrackStemstrButton track={track} />
|
||||
<TrackMenu track={track} aria-label="Options" />
|
||||
</ButtonGroup>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
24
src/views/tracks/components/track-download-button.tsx
Normal file
24
src/views/tracks/components/track-download-button.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import { IconButton, IconButtonProps, Link } from "@chakra-ui/react";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { getDownloadURL } from "../../../helpers/nostr/stemstr";
|
||||
import { DownloadIcon } from "../../../components/icons";
|
||||
|
||||
export default function TrackDownloadButton({
|
||||
track,
|
||||
...props
|
||||
}: { track: NostrEvent } & Omit<IconButtonProps, "aria-label" | "icon">) {
|
||||
const download = getDownloadURL(track);
|
||||
|
||||
return download ? (
|
||||
<IconButton
|
||||
as={Link}
|
||||
icon={<DownloadIcon />}
|
||||
aria-label="Download"
|
||||
title="Download"
|
||||
href={download.url}
|
||||
download
|
||||
isExternal
|
||||
{...props}
|
||||
/>
|
||||
) : null;
|
||||
}
|
37
src/views/tracks/components/track-menu.tsx
Normal file
37
src/views/tracks/components/track-menu.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { MenuItem, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { CustomMenuIconButton, MenuIconButtonProps } from "../../../components/menu-icon-button";
|
||||
import OpenInAppMenuItem from "../../../components/common-menu-items/open-in-app";
|
||||
import CopyShareLinkMenuItem from "../../../components/common-menu-items/copy-share-link";
|
||||
import CopyEmbedCodeMenuItem from "../../../components/common-menu-items/copy-embed-code";
|
||||
import MuteUserMenuItem from "../../../components/common-menu-items/mute-user";
|
||||
import { CodeIcon } from "../../../components/icons";
|
||||
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
|
||||
|
||||
export default function TrackMenu({
|
||||
track,
|
||||
detailsClick,
|
||||
...props
|
||||
}: { track: NostrEvent; detailsClick?: () => void } & Omit<MenuIconButtonProps, "children">) {
|
||||
const debugModal = useDisclosure();
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomMenuIconButton {...props}>
|
||||
<OpenInAppMenuItem event={track} />
|
||||
<CopyShareLinkMenuItem event={track} />
|
||||
<CopyEmbedCodeMenuItem event={track} />
|
||||
<MuteUserMenuItem event={track} />
|
||||
|
||||
<MenuItem onClick={debugModal.onOpen} icon={<CodeIcon />}>
|
||||
View Raw
|
||||
</MenuItem>
|
||||
</CustomMenuIconButton>
|
||||
|
||||
{debugModal.isOpen && (
|
||||
<NoteDebugModal event={track} isOpen={debugModal.isOpen} onClose={debugModal.onClose} size="6xl" />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
43
src/views/tracks/components/track-player.tsx
Normal file
43
src/views/tracks/components/track-player.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import { Box, Flex, IconButton, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
import { LiveAudioPlayer } from "../../../components/live-audio-player";
|
||||
import { getDownloadURL, getStreamURL, getWaveform } from "../../../helpers/nostr/stemstr";
|
||||
import Play from "../../../components/icons/play";
|
||||
|
||||
export default function TrackPlayer({ track }: { track: NostrEvent }) {
|
||||
const streamUrl = getStreamURL(track);
|
||||
const downloadUrl = getDownloadURL(track);
|
||||
const waveform = getWaveform(track);
|
||||
|
||||
const showPlayer = useDisclosure();
|
||||
|
||||
if (!showPlayer.isOpen)
|
||||
return (
|
||||
<Flex
|
||||
gap="2px"
|
||||
h="20"
|
||||
alignItems="center"
|
||||
px="4"
|
||||
py="2"
|
||||
overflow="hidden"
|
||||
borderColor="primary.500"
|
||||
borderWidth="1px"
|
||||
borderRadius="lg"
|
||||
position="relative"
|
||||
>
|
||||
<IconButton aria-label="Play" mr="4" icon={<Play />} onClick={showPlayer.onOpen} size="md" variant="outline" />
|
||||
{waveform?.map((v) => <Box h={v + "%"} w="3px" bg="primary.800" />)}
|
||||
</Flex>
|
||||
);
|
||||
|
||||
if (streamUrl) {
|
||||
return <LiveAudioPlayer stream={streamUrl.url} w="full" autoPlay h="20" />;
|
||||
} else if (downloadUrl) {
|
||||
return (
|
||||
<Box as="audio" controls w="full" autoPlay h="20">
|
||||
<source src={downloadUrl.url} type={downloadUrl.format} />
|
||||
</Box>
|
||||
);
|
||||
} else return null;
|
||||
}
|
16
src/views/tracks/components/track-stemstr-button.tsx
Normal file
16
src/views/tracks/components/track-stemstr-button.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { Button, Image, Link } from "@chakra-ui/react";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
|
||||
export default function TrackStemstrButton({ track }: { track: NostrEvent }) {
|
||||
return (
|
||||
<Button
|
||||
as={Link}
|
||||
leftIcon={<Image src="https://stemstr.app/favicon.svg" />}
|
||||
href={`https://stemstr.app/thread/${track.id}`}
|
||||
colorScheme="purple"
|
||||
isExternal
|
||||
>
|
||||
View on Stemstr
|
||||
</Button>
|
||||
);
|
||||
}
|
58
src/views/tracks/index.tsx
Normal file
58
src/views/tracks/index.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { useCallback } from "react";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import { STEMSTR_RELAY, STEMSTR_TRACK_KIND } from "../../helpers/nostr/stemstr";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
|
||||
import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/local/relay-selection-provider";
|
||||
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
|
||||
import TrackCard from "./components/track-card";
|
||||
import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
function TracksPage() {
|
||||
const { listId, filter } = usePeopleListContext();
|
||||
const { relays } = useRelaySelectionContext();
|
||||
|
||||
const clientMuteFilter = useClientSideMuteFilter();
|
||||
const eventFilter = useCallback(
|
||||
(event: NostrEvent) => {
|
||||
if (clientMuteFilter(event)) return false;
|
||||
return true;
|
||||
},
|
||||
[clientMuteFilter],
|
||||
);
|
||||
const timeline = useTimelineLoader(`${listId}-tracks`, relays, filter && { kinds: [STEMSTR_TRACK_KIND], ...filter }, {
|
||||
eventFilter,
|
||||
});
|
||||
const tracks = useSubject(timeline.timeline);
|
||||
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<VerticalPageLayout>
|
||||
<Flex gap="2" wrap="wrap">
|
||||
<PeopleListSelection />
|
||||
</Flex>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{tracks.map((track) => (
|
||||
<TrackCard track={track} />
|
||||
))}
|
||||
</IntersectionObserverProvider>
|
||||
</VerticalPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function TracksView() {
|
||||
return (
|
||||
<PeopleListProvider>
|
||||
<RelaySelectionProvider additionalDefaults={[STEMSTR_RELAY]}>
|
||||
<TracksPage />
|
||||
</RelaySelectionProvider>
|
||||
</PeopleListProvider>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user