Skip to content

Commit

Permalink
Add support for Olas media posts
Browse files Browse the repository at this point in the history
  • Loading branch information
hzrd149 committed Dec 10, 2024
1 parent 3fc9c64 commit 2f1d50a
Show file tree
Hide file tree
Showing 29 changed files with 950 additions and 227 deletions.
5 changes: 5 additions & 0 deletions .changeset/beige-dragons-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostrudel": minor
---

Add support for NIP-22 comments on media posts
5 changes: 5 additions & 0 deletions .changeset/violet-mails-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostrudel": minor
---

Add support for Olas media posts
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
"@codemirror/autocomplete": "^6.18.3",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/language": "^6.10.6",
"@codemirror/view": "^6.35.2",
"@emotion/react": "^11.13.5",
"@emotion/styled": "^11.13.5",
"@codemirror/view": "^6.35.3",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@getalby/bitcoin-connect": "^3.6.3",
"@getalby/bitcoin-connect-react": "^3.6.3",
"@noble/ciphers": "^1.1.3",
Expand All @@ -45,6 +45,7 @@
"applesauce-channel": "next",
"applesauce-content": "next",
"applesauce-core": "next",
"applesauce-factory": "next",
"applesauce-lists": "next",
"applesauce-net": "next",
"applesauce-react": "next",
Expand Down Expand Up @@ -83,6 +84,7 @@
"nostr-idb": "^2.2.0",
"nostr-tools": "^2.10.4",
"nostr-wasm": "^0.1.0",
"nuka-carousel": "^8.1.1",
"prettier": "^3.4.2",
"react": "^18.3.1",
"react-chartjs-2": "^5.2.0",
Expand All @@ -99,7 +101,7 @@
"react-router-dom": "^6.28.0",
"react-simplemde-editor": "^5.2.0",
"react-singleton-hook": "^4.0.1",
"react-use": "^17.5.1",
"react-use": "^17.6.0",
"react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10",
"remark-gfm": "^4.0.0",
Expand Down Expand Up @@ -132,7 +134,7 @@
"@types/lodash.throttle": "^4.1.9",
"@types/ngeohash": "^0.6.8",
"@types/react": "^18.3.14",
"@types/react-dom": "^18.3.2",
"@types/react-dom": "^18.3.3",
"@types/react-window": "^1.8.8",
"@types/three": "^0.160.0",
"@types/webscopeio__react-textarea-autocomplete": "^4.7.5",
Expand Down
428 changes: 234 additions & 194 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const DVMFeedView = lazy(() => import("./views/discovery/dvm-feed/feed"));
const BlindspotHomeView = lazy(() => import("./views/discovery/blindspot"));
const BlindspotFeedView = lazy(() => import("./views/discovery/blindspot/feed"));
const RelayDiscoveryView = lazy(() => import("./views/discovery/relays/index"));
const MediaFeedView = lazy(() => import("./views/media/index"));
const MediaPostView = lazy(() => import("./views/media/media-post"));
import SettingsView from "./views/settings";
import NostrLinkView from "./views/link";
import ProfileView from "./views/profile";
Expand Down Expand Up @@ -106,6 +108,7 @@ import ArticlesHomeView from "./views/articles";
import ArticleView from "./views/articles/article";
import WalletView from "./views/wallet";
import SupportView from "./views/support";
import UserMediaPostsTab from "./views/user/media-posts";
const TracksView = lazy(() => import("./views/tracks"));
const UserTracksTab = lazy(() => import("./views/user/tracks"));
const UserVideosTab = lazy(() => import("./views/user/videos"));
Expand Down Expand Up @@ -258,6 +261,7 @@ const router = createHashRouter([
{ path: "about", element: <UserAboutTab /> },
{ path: "notes", element: <UserNotesTab /> },
{ path: "articles", element: <UserArticlesTab /> },
{ path: "media", element: <UserMediaPostsTab /> },
{ path: "streams", element: <UserStreamsTab /> },
{ path: "tracks", element: <UserTracksTab /> },
{ path: "videos", element: <UserVideosTab /> },
Expand Down Expand Up @@ -365,6 +369,13 @@ const router = createHashRouter([
},
],
},
{
path: "media",
children: [
{ path: "", element: <MediaFeedView /> },
{ path: ":pointer", element: <MediaPostView /> },
],
},
{
path: "wiki",
children: [
Expand Down
5 changes: 3 additions & 2 deletions src/components/content/links/video.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { lazy } from "react";
import { lazy, VideoHTMLAttributes } from "react";
import styled from "@emotion/styled";

import { isStreamURL, isVideoURL } from "../../../helpers/url";
Expand All @@ -15,7 +15,7 @@ const StyledVideo = styled.video`
z-index: 1;
`;

function TrustVideo({ src }: { src: string }) {
export function TrustVideo({ src, ...props }: { src: string } & VideoHTMLAttributes<HTMLVideoElement>) {
const { blurImages } = useAppSettings();
const { onClick, handleEvent, style } = useElementTrustBlur();

Expand All @@ -26,6 +26,7 @@ function TrustVideo({ src }: { src: string }) {
style={blurImages ? style : undefined}
onClick={blurImages ? onClick : undefined}
onPlay={blurImages ? handleEvent : undefined}
{...props}
/>
);
}
Expand Down
2 changes: 2 additions & 0 deletions src/components/embed-event/event-types/embedded-reaction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Timestamp from "../../timestamp";
import ReactionIcon from "../../event-reactions/reaction-icon";
import { NoteLink } from "../../note/note-link";
import { nip25 } from "nostr-tools";
import DebugEventButton from "../../debug-modal/debug-event-button";

export default function EmbeddedReaction({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
const pointer = nip25.getReactedEventPointer(event);
Expand All @@ -24,6 +25,7 @@ export default function EmbeddedReaction({ event, ...props }: Omit<CardProps, "c
{pointer && <NoteLink noteId={pointer.id} />}
<Spacer />
<Timestamp timestamp={event.created_at} />
<DebugEventButton event={event} variant="ghost" size="xs" />
</Flex>
</Card>
</TrustProvider>
Expand Down
1 change: 1 addition & 0 deletions src/components/event-reactions/reaction-group-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export default function ReactionGroupButton({
if (count <= 1) {
return <IconButton icon={<ReactionIcon emoji={emoji} url={url} />} aria-label="Reaction" {...props} />;
}

return (
<Button leftIcon={<ReactionIcon emoji={emoji} url={url} />} title={emoji} {...props}>
{count > 1 && count}
Expand Down
62 changes: 62 additions & 0 deletions src/components/media-post/media-post-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Box, Button, ButtonGroup, Card, CardBody, CardFooter, CardHeader } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { Link as RouterLink } from "react-router-dom";

import UserAvatarLink from "../user/user-avatar-link";
import UserLink from "../user/user-link";
import UserDnsIdentity from "../user/user-dns-identity";
import DebugEventButton from "../debug-modal/debug-event-button";
import { TrustProvider } from "../../providers/local/trust-provider";
import EventReactionButtons from "../event-reactions/event-reactions";
import AddReactionButton from "../note/timeline-note/components/add-reaction-button";
import RepostButton from "../note/timeline-note/components/repost-button";
import QuoteEventButton from "../note/quote-event-button";
import MediaPostSlides from "./media-slides";
import MediaPostContents from "./media-post-content";
import { getSharableEventAddress } from "../../services/event-relay-hint";
import { ThreadIcon } from "../icons";
import EventZapIconButton from "../zap/event-zap-icon-button";
import Timestamp from "../timestamp";

export default function MediaPost({ post }: { post: NostrEvent }) {
const nevent = getSharableEventAddress(post);

return (
<TrustProvider event={post}>
<Card maxW="2xl" mx="auto">
<CardHeader display="flex" alignItems="center" gap="2" p="2">
<UserAvatarLink pubkey={post.pubkey} />
<Box>
<UserLink pubkey={post.pubkey} fontWeight="bold" /> <Timestamp timestamp={post.created_at} />
<br />
<UserDnsIdentity pubkey={post.pubkey} />
</Box>

<Button as={RouterLink} to={`/media/${nevent}`} leftIcon={<ThreadIcon boxSize={5} />} ml="auto">
Comments
</Button>
</CardHeader>

<CardBody p="0" position="relative" display="flex" flexDirection="column" gap="2" minH="md">
<MediaPostSlides post={post} />

{post.content.length > 0 && <MediaPostContents post={post} px="2" />}
</CardBody>

<CardFooter p="2" display="flex" gap="2">
<ButtonGroup size="sm" variant="ghost">
<EventZapIconButton event={post} aria-label="Zap post" />
<AddReactionButton event={post} />
<EventReactionButtons event={post} max={4} />
</ButtonGroup>

<ButtonGroup size="sm" variant="ghost" ml="auto">
<RepostButton event={post} />
<QuoteEventButton event={post} />
<DebugEventButton event={post} variant="ghost" ml="auto" size="sm" alignSelf="flex-start" />
</ButtonGroup>
</CardFooter>
</Card>
</TrustProvider>
);
}
28 changes: 28 additions & 0 deletions src/components/media-post/media-post-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Box, BoxProps } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { useRenderedContent } from "applesauce-react/hooks";
import { emojis, nostrMentions, links, hashtags } from "applesauce-content/text";

import { components } from "../content";
import { renderGenericUrl } from "../content/links";
import { nipDefinitions } from "../content/transform/nip-notation";

const transformers = [links, nostrMentions, emojis, hashtags, nipDefinitions];

const linkRenderers = [renderGenericUrl];

const MediaPostContentSymbol = Symbol.for("media-post-content");

export default function MediaPostContents({ post, ...props }: { post: NostrEvent } & Omit<BoxProps, "children">) {
const content = useRenderedContent(post, components, {
linkRenderers,
transformers,
cacheKey: MediaPostContentSymbol,
});

return (
<Box whiteSpace="pre-wrap" dir="auto" {...props}>
{content}
</Box>
);
}
141 changes: 141 additions & 0 deletions src/components/media-post/media-slides.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { Box, Flex, FlexProps, IconButton, Spacer } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { getMediaAttachments, MediaAttachment } from "applesauce-core/helpers/media-attachment";
import { Carousel, useCarousel } from "nuka-carousel";
import styled from "@emotion/styled";

import { TrustImage, TrustVideo } from "../content/links";
import { isImageURL, isVideoURL } from "applesauce-core/helpers";
import { ChevronLeftIcon, ChevronRightIcon } from "../icons";
import ZapBubbles from "../note/timeline-note/components/zap-bubbles";

function CustomArrows() {
const { currentPage, totalPages, wrapMode, goBack, goForward } = useCarousel();

const allowWrap = wrapMode === "wrap";
const enablePrevNavButton = allowWrap || currentPage > 0;
const enableNextNavButton = allowWrap || currentPage < totalPages - 1;

return (
<Flex justifyContent="space-between" position="absolute" top="50%" right="0" left="0">
<IconButton
icon={<ChevronLeftIcon boxSize={8} />}
onClick={goBack}
aria-label="previous image"
variant="ghost"
h="24"
w="12"
isDisabled={!enablePrevNavButton}
>
PREV
</IconButton>
<IconButton
icon={<ChevronRightIcon boxSize={8} />}
onClick={goForward}
aria-label="next image"
variant="ghost"
h="24"
w="12"
isDisabled={!enableNextNavButton}
>
NEXT
</IconButton>
</Flex>
);
}

function cls(...classes: string[]) {
return classes.filter(Boolean).join(" ");
}
function PageIndicators() {
const { totalPages, currentPage, goToPage } = useCarousel();

const className = (index: number) =>
cls("nuka-page-indicator", currentPage === index ? "nuka-page-indicator-active" : "");

return (
<div className="nuka-page-container" data-testid="pageIndicatorContainer">
{[...Array(totalPages)].map((_, index) => (
<button key={index} onClick={() => goToPage(index)} className={className(index)}>
<span className="nuka-hidden">{index + 1}</span>
</button>
))}
</div>
);
}

function MediaAttachmentSlide({ media }: { media: MediaAttachment }) {
if (media.type?.startsWith("video/") || isVideoURL(media.url)) {
return <TrustVideo src={media.url} poster={media.image} aria-description={media.alt} />;
} else if (media.type?.startsWith("image/") || isImageURL(media.url)) {
return <TrustImage src={media.url} alt={media.alt} maxH="full" />;
}

return (
<Box aspectRatio={1} minW="lg">
Unknown media type {media.type ?? "Unknown"}
</Box>
);
}

const CustomCarousel = styled(Carousel)`
& {
height: 100%;
overflow: hidden;
}
.nuka-slide-container {
height: 100%;
overflow: hidden;
}
.nuka-overflow {
overflow-x: scroll;
overflow-y: hidden;
height: 100%;
}
.nuka-wrapper {
height: 100%;
}
`;

export default function MediaPostSlides({
post,
showZaps = true,
...props
}: { post: NostrEvent; showZaps?: boolean } & Omit<FlexProps, "children">) {
const attachments = getMediaAttachments(post);

if (attachments.length === 1)
return (
<Flex gap="2" direction="column" {...props}>
<Flex justifyContent="center" overflow="hidden" flexGrow={1} alignItems="flex-start">
<MediaAttachmentSlide media={attachments[0]} />
</Flex>
{showZaps && <ZapBubbles event={post} px="2" />}
</Flex>
);

return (
<Flex gap="2" direction="column" {...props}>
<CustomCarousel
scrollDistance="screen"
showDots
arrows={<CustomArrows />}
showArrows
dots={
<Flex gap="2" justifyContent="space-between" alignItems="center" px="2">
{showZaps && <ZapBubbles event={post} />}
<Spacer />
<PageIndicators />
</Flex>
}
>
{attachments.map((media) => (
<MediaAttachmentSlide key={media.sha256 || media.url} media={media} />
))}
</CustomCarousel>
</Flex>
);
}
1 change: 1 addition & 0 deletions src/components/note/note-zap-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export default function NoteZapButton({ event, allowComment, showEventPreview, .
icon={<LightningIcon verticalAlign="sub" />}
aria-label="Zap Note"
title="Zap Note"
colorScheme={hasZapped ? "primary" : undefined}
{...props}
onClick={onOpen}
isDisabled={!canZap}
Expand Down
Loading

0 comments on commit 2f1d50a

Please sign in to comment.