From 85a9dad33a545225d14a99c76b2bbd62d2843b84 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Thu, 12 Oct 2023 14:27:39 -0500 Subject: [PATCH] add stemstr embeds --- .changeset/cuddly-carrots-camp.md | 5 + src/app.tsx | 2 + .../embed-event/event-types/embedded-list.tsx | 2 +- .../event-types/embedded-stemstr-track.tsx | 101 ++++++++++++++++++ src/components/embed-event/index.tsx | 4 + src/components/icons.tsx | 2 + src/components/live-audio-player.tsx | 52 +++++++++ src/helpers/nostr/stemstr.ts | 30 ++++++ src/views/streams/index.tsx | 8 +- src/views/user/index.tsx | 1 + src/views/user/tracks.tsx | 51 +++++++++ 11 files changed, 255 insertions(+), 3 deletions(-) create mode 100644 .changeset/cuddly-carrots-camp.md create mode 100644 src/components/embed-event/event-types/embedded-stemstr-track.tsx create mode 100644 src/components/live-audio-player.tsx create mode 100644 src/helpers/nostr/stemstr.ts create mode 100644 src/views/user/tracks.tsx diff --git a/.changeset/cuddly-carrots-camp.md b/.changeset/cuddly-carrots-camp.md new file mode 100644 index 000000000..44e1280ab --- /dev/null +++ b/.changeset/cuddly-carrots-camp.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add stemstr embeds diff --git a/src/app.tsx b/src/app.tsx index cbd912a61..ca4359686 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -66,6 +66,7 @@ import RelaysView from "./views/relays"; import RelayView from "./views/relays/relay"; import RelayReviewsView from "./views/relays/reviews"; import PopularRelaysView from "./views/relays/popular"; +import UserTracksTab from "./views/user/tracks"; const ToolsHomeView = React.lazy(() => import("./views/tools")); const NetworkView = React.lazy(() => import("./views/tools/network")); @@ -163,6 +164,7 @@ const router = createHashRouter([ { path: "notes", element: }, { path: "articles", element: }, { path: "streams", element: }, + { path: "tracks", element: }, { path: "zaps", element: }, { path: "likes", element: }, { path: "lists", element: }, diff --git a/src/components/embed-event/event-types/embedded-list.tsx b/src/components/embed-event/event-types/embedded-list.tsx index 6a98251d4..44fb75d3f 100644 --- a/src/components/embed-event/event-types/embedded-list.tsx +++ b/src/components/embed-event/event-types/embedded-list.tsx @@ -10,7 +10,7 @@ import { UserLink } from "../../user-link"; import ListFeedButton from "../../../views/lists/components/list-feed-button"; import { ListCardContent } from "../../../views/lists/components/list-card"; -export default function EmbeddedList({ list: list, ...props }: Omit & { list: NostrEvent }) { +export default function EmbeddedList({ list, ...props }: Omit & { list: NostrEvent }) { const link = isSpecialListKind(list.kind) ? createCoordinate(list.kind, list.pubkey) : getSharableEventAddress(list); return ( diff --git a/src/components/embed-event/event-types/embedded-stemstr-track.tsx b/src/components/embed-event/event-types/embedded-stemstr-track.tsx new file mode 100644 index 000000000..da4551231 --- /dev/null +++ b/src/components/embed-event/event-types/embedded-stemstr-track.tsx @@ -0,0 +1,101 @@ +import { + Box, + Button, + ButtonGroup, + Card, + CardBody, + CardFooter, + CardHeader, + CardProps, + Flex, + IconButton, + Image, + Link, + Tag, + Tooltip, +} from "@chakra-ui/react"; + +import { NostrEvent } from "../../../types/nostr-event"; +import { UserAvatarLink } from "../../user-avatar-link"; +import { UserLink } from "../../user-link"; +import { InlineNoteContent } from "../../note/inline-note-content"; +import { getDownloadURL, getHashtags, getStreamURL } from "../../../helpers/nostr/stemstr"; +import { DownloadIcon, ReplyIcon } from "../../icons"; +import NoteZapButton from "../../note/note-zap-button"; +import { QuoteRepostButton } from "../../note/components/quote-repost-button"; +import Timestamp from "../../timestamp"; +import { ReactNode } from "react"; +import { LiveAudioPlayer } from "../../live-audio-player"; + +// example nevent1qqst32cnyhhs7jt578u7vp3y047dduuwjquztpvwqc43f3nvg8dh28gpzamhxue69uhhyetvv9ujuum5v4khxarj9eshquq4rxdxa +export default function EmbeddedStemstrTrack({ track, ...props }: Omit & { track: NostrEvent }) { + const streamUrl = getStreamURL(track); + const downloadUrl = getDownloadURL(track); + + let player: ReactNode | null = null; + if (streamUrl) { + player = ; + } else if (downloadUrl) { + player = ( + + + + ); + } + + const hashtags = getHashtags(track); + + return ( + + + + + + + + {player} + + {hashtags.length > 0 && ( + + {hashtags.map((hashtag) => ( + #{hashtag} + ))} + + )} + + + + + + + + + + + {downloadUrl && ( + } + aria-label="Download" + title="Download" + href={downloadUrl.url} + download + isExternal + /> + )} + + + + + ); +} diff --git a/src/components/embed-event/index.tsx b/src/components/embed-event/index.tsx index b3b6d13e6..b402ed6c9 100644 --- a/src/components/embed-event/index.tsx +++ b/src/components/embed-event/index.tsx @@ -23,6 +23,8 @@ import EmbeddedBadge from "./event-types/embedded-badge"; import EmbeddedStreamMessage from "./event-types/embedded-stream-message"; import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities"; import EmbeddedCommunity from "./event-types/embedded-community"; +import { STEMSTR_TRACK_KIND } from "../../helpers/nostr/stemstr"; +import EmbeddedStemstrTrack from "./event-types/embedded-stemstr-track"; export type EmbedProps = { goalProps?: EmbeddedGoalOptions; @@ -53,6 +55,8 @@ export function EmbedEvent({ return ; case COMMUNITY_DEFINITION_KIND: return ; + case STEMSTR_TRACK_KIND: + return ; } return ; diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 53537bb4b..dbfebcfd9 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -59,6 +59,7 @@ import Plus from "./icons/plus"; import Bookmark from "./icons/bookmark"; import BankNote01 from "./icons/bank-note-01"; import Wallet02 from "./icons/wallet-02"; +import Download01 from "./icons/download-01"; const defaultProps: IconProps = { boxSize: 4 }; @@ -226,3 +227,4 @@ export const GhostIcon = createIcon({ export const ECashIcon = BankNote01; export const WalletIcon = Wallet02; +export const DownloadIcon = Download01 diff --git a/src/components/live-audio-player.tsx b/src/components/live-audio-player.tsx new file mode 100644 index 000000000..03c9186d2 --- /dev/null +++ b/src/components/live-audio-player.tsx @@ -0,0 +1,52 @@ +import { HTMLProps, useEffect, useRef } from "react"; +import { Box, BoxProps } from "@chakra-ui/react"; +import Hls from "hls.js"; + +export function LiveAudioPlayer({ + stream, + autoPlay, + poster, + muted, + ...props +}: Omit & { + stream?: string; + autoPlay?: boolean; + poster?: string; + muted?: HTMLProps["muted"]; +}) { + const audio = useRef(null); + + useEffect(() => { + if (stream && audio.current && !audio.current.src && Hls.isSupported()) { + try { + const hls = new Hls({ capLevelToPlayerSize: true }); + hls.loadSource(stream); + hls.attachMedia(audio.current); + hls.on(Hls.Events.ERROR, (event, data) => { + const errorType = data.type; + if (errorType === Hls.ErrorTypes.NETWORK_ERROR && data.fatal) { + hls.stopLoad(); + hls.detachMedia(); + } + }); + hls.on(Hls.Events.MANIFEST_PARSED, () => {}); + return () => hls.destroy(); + } catch (e) { + console.error(e); + } + } + }, [audio, stream]); + + return ( + + ); +} diff --git a/src/helpers/nostr/stemstr.ts b/src/helpers/nostr/stemstr.ts new file mode 100644 index 000000000..6f97ce7f8 --- /dev/null +++ b/src/helpers/nostr/stemstr.ts @@ -0,0 +1,30 @@ +import { NostrEvent } from "../../types/nostr-event"; + +export const STEMSTR_TRACK_KIND = 1808; + +export function getSha256Hash(track: NostrEvent) { + return track.tags.find((t) => t[0] === "x")?.[1]; +} +export function getWaveform(track: NostrEvent) { + const tag = track.tags.find((t) => t[0] === "waveform"); + if (tag?.[1]) return JSON.parse(tag[1]) as number[]; +} +export function getHashtags(track: NostrEvent) { + return track.tags.filter((t) => t[0] === "t" && t[1]).map((t) => t[1] as string); +} +export function getDownloadURL(track: NostrEvent) { + const tag = track.tags.find((t) => t[0] === "download_url"); + if (!tag) return; + const url = tag[1]; + if (!url) throw new Error("missing download url"); + const format = tag[2]; + return { url, format }; +} +export function getStreamURL(track: NostrEvent) { + const tag = track.tags.find((t) => t[0] === "stream_url"); + if (!tag) return; + const url = tag[1]; + if (!url) throw new Error("missing download url"); + const format = tag[2]; + return { url, format }; +} diff --git a/src/views/streams/index.tsx b/src/views/streams/index.tsx index b77733a48..6eb03ec8a 100644 --- a/src/views/streams/index.tsx +++ b/src/views/streams/index.tsx @@ -61,13 +61,17 @@ function StreamsPage() { + + Live + {liveStreams.map((stream) => ( ))} - Ended - + + Ended + {endedStreams.map((stream) => ( diff --git a/src/views/user/index.tsx b/src/views/user/index.tsx index e062b9bec..d50ec664d 100644 --- a/src/views/user/index.tsx +++ b/src/views/user/index.tsx @@ -56,6 +56,7 @@ const tabs = [ { label: "Likes", path: "likes" }, { label: "Relays", path: "relays" }, { label: "Goals", path: "goals" }, + { label: "Tracks", path: "tracks" }, { label: "Emoji Packs", path: "emojis" }, { label: "Reports", path: "reports" }, { label: "Followers", path: "followers" }, diff --git a/src/views/user/tracks.tsx b/src/views/user/tracks.tsx new file mode 100644 index 000000000..62694a65a --- /dev/null +++ b/src/views/user/tracks.tsx @@ -0,0 +1,51 @@ +import { useRef } from "react"; +import { useOutletContext } from "react-router-dom"; +import { Box, SimpleGrid } from "@chakra-ui/react"; + +import { useAdditionalRelayContext } from "../../providers/additional-relay-context"; +import useTimelineLoader from "../../hooks/use-timeline-loader"; +import useSubject from "../../hooks/use-subject"; +import { getEventUID } from "../../helpers/nostr/events"; +import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer"; +import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; +import VerticalPageLayout from "../../components/vertical-page-layout"; +import { STEMSTR_TRACK_KIND } from "../../helpers/nostr/stemstr"; +import EmbeddedStemstrTrack from "../../components/embed-event/event-types/embedded-stemstr-track"; +import { unique } from "../../helpers/array"; +import { NostrEvent } from "../../types/nostr-event"; + +function Track({ track }: { track: NostrEvent }) { + const ref = useRef(null); + useRegisterIntersectionEntity(ref, getEventUID(track)); + + return ( + + + + ); +} + +export default function UserTracksTab() { + const { pubkey } = useOutletContext() as { pubkey: string }; + const readRelays = useAdditionalRelayContext(); + + const timeline = useTimelineLoader(pubkey + "-tracks", unique([...readRelays, "wss://relay.stemstr.app"]), { + authors: [pubkey], + kinds: [STEMSTR_TRACK_KIND], + }); + const tracks = useSubject(timeline.timeline); + + const callback = useTimelineCurserIntersectionCallback(timeline); + + return ( + + + + {tracks.map((track) => ( + + ))} + + + + ); +}