make notes clickable

This commit is contained in:
hzrd149 2023-10-13 16:02:18 -05:00
parent 07d5e7719e
commit d2f307642a
9 changed files with 99 additions and 40 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Make notes clickable

View File

@ -15,13 +15,7 @@ import Timestamp from "../../timestamp";
import { getSharableEventAddress } from "../../../helpers/nip19"; import { getSharableEventAddress } from "../../../helpers/nip19";
import { InlineNoteContent } from "../../note/inline-note-content"; import { InlineNoteContent } from "../../note/inline-note-content";
import { useNavigateInDrawer } from "../../../providers/drawer-sub-view-provider"; import { useNavigateInDrawer } from "../../../providers/drawer-sub-view-provider";
import styled from "@emotion/styled"; import HoverLinkOverlay from "../../hover-link-overlay";
const HoverLinkOverlay = styled(LinkOverlay)`
&:hover:before {
background-color: var(--chakra-colors-card-hover-overlay);
}
`;
export default function EmbeddedNote({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) { export default function EmbeddedNote({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
const { showSignatureVerification } = useSubject(appSettings); const { showSignatureVerification } = useSubject(appSettings);

View File

@ -8,7 +8,7 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { Image, ImageProps } from "@chakra-ui/react"; import { Image, ImageProps, Link, LinkProps } from "@chakra-ui/react";
import appSettings from "../../services/settings/app-settings"; import appSettings from "../../services/settings/app-settings";
import { useTrusted } from "../../providers/trust"; import { useTrusted } from "../../providers/trust";
@ -61,23 +61,69 @@ export const TrustImage = forwardRef<HTMLImageElement, TrustImageProps>((props,
else return <Image {...props} onClick={handleClick} style={{ ...style, ...props.style }} ref={ref} />; else return <Image {...props} onClick={handleClick} style={{ ...style, ...props.style }} ref={ref} />;
}); });
export type EmbeddedImageProps = TrustImageProps & { export type EmbeddedImageProps = Omit<LinkProps, "children" | "href" | "onClick"> & {
src?: string;
event?: NostrEvent; event?: NostrEvent;
imageProps?: TrustImageProps;
}; };
export const EmbeddedImage = forwardRef<HTMLImageElement, EmbeddedImageProps>(({ src, event, ...props }, ref) => { function useImageThumbnail(src?: string) {
const thumbnail = appSettings.value.imageProxy return appSettings.value.imageProxy ? new URL(`/256,fit/${src}`, appSettings.value.imageProxy).toString() : src;
? new URL(`/256,fit/${src}`, appSettings.value.imageProxy).toString() }
: src;
ref = ref || useRef<HTMLImageElement | null>(null); export const EmbeddedImage = forwardRef<HTMLImageElement, EmbeddedImageProps>(
const { show } = useRegisterSlide( ({ src, event, imageProps, ...props }, ref) => {
ref as MutableRefObject<HTMLImageElement | null>, const thumbnail = useImageThumbnail(src);
src ? { type: "image", src, event } : undefined,
);
return <TrustImage {...props} src={thumbnail} cursor="pointer" ref={ref} onClick={show} />; ref = ref || useRef<HTMLImageElement | null>(null);
}); const { show } = useRegisterSlide(
ref as MutableRefObject<HTMLImageElement | null>,
src ? { type: "image", src, event } : undefined,
);
const handleClick = useCallback<MouseEventHandler<HTMLElement>>(
(e) => {
!e.isPropagationStopped() && show();
e.preventDefault();
},
[show],
);
// NOTE: the parent <div> has display=block and and <a> has inline-block
// this is so that the <a> element can act like a block without being full width
return (
<div>
<Link href={src} isExternal onClick={handleClick} display="inline-block" {...props}>
<TrustImage {...imageProps} src={thumbnail} cursor="pointer" ref={ref} onClick={handleClick} />
</Link>
</div>
);
},
);
export const GalleryImage = forwardRef<HTMLImageElement, EmbeddedImageProps>(
({ src, event, imageProps, ...props }, ref) => {
const thumbnail = useImageThumbnail(src);
ref = ref || useRef<HTMLImageElement | null>(null);
const { show } = useRegisterSlide(
ref as MutableRefObject<HTMLImageElement | null>,
src ? { type: "image", src, event } : undefined,
);
const handleClick = useCallback<MouseEventHandler<HTMLElement>>(
(e) => {
!e.isPropagationStopped() && show();
e.preventDefault();
},
[show],
);
return (
<Link href={src} isExternal onClick={handleClick} {...props}>
<TrustImage src={thumbnail} cursor="pointer" ref={ref} onClick={handleClick} {...imageProps} />
</Link>
);
},
);
export function ImageGallery({ images, event }: { images: string[]; event?: NostrEvent }) { export function ImageGallery({ images, event }: { images: string[]; event?: NostrEvent }) {
const photos = useMemo(() => { const photos = useMemo(() => {
@ -93,7 +139,9 @@ export function ImageGallery({ images, event }: { images: string[]; event?: Nost
<PhotoGallery <PhotoGallery
layout="rows" layout="rows"
photos={photos} photos={photos}
renderPhoto={({ photo, imageProps, wrapperStyle }) => <EmbeddedImage {...imageProps} />} renderPhoto={({ photo, imageProps, wrapperStyle }) => (
<GalleryImage src={imageProps.src} style={imageProps.style} />
)}
targetRowHeight={(containerWidth) => containerWidth / rowMultiplier} targetRowHeight={(containerWidth) => containerWidth / rowMultiplier}
/> />
); );
@ -165,5 +213,5 @@ export function embedImageGallery(content: EmbedableContent, event?: NostrEvent)
export function renderImageUrl(match: URL) { export function renderImageUrl(match: URL) {
if (!isImageURL(match)) return null; if (!isImageURL(match)) return null;
return <EmbeddedImage src={match.toString()} maxH={["initial", "35vh"]} />; return <EmbeddedImage src={match.toString()} imageProps={{ maxH: ["initial", "35vh"] }} />;
} }

View File

@ -0,0 +1,10 @@
import { LinkOverlay } from "@chakra-ui/react";
import styled from "@emotion/styled";
const HoverLinkOverlay = styled(LinkOverlay)`
&:hover:before {
background-color: var(--chakra-colors-card-hover-overlay);
}
`;
export default HoverLinkOverlay;

View File

@ -10,6 +10,7 @@ import {
Flex, Flex,
IconButton, IconButton,
Link, Link,
LinkBox,
Text, Text,
useDisclosure, useDisclosure,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
@ -44,14 +45,16 @@ import { getSharableEventAddress } from "../../helpers/nip19";
import { COMMUNITY_DEFINITION_KIND, getCommunityName } from "../../helpers/nostr/communities"; import { COMMUNITY_DEFINITION_KIND, getCommunityName } from "../../helpers/nostr/communities";
import useReplaceableEvent from "../../hooks/use-replaceable-event"; import useReplaceableEvent from "../../hooks/use-replaceable-event";
import { useBreakpointValue } from "../../providers/breakpoint-provider"; import { useBreakpointValue } from "../../providers/breakpoint-provider";
import HoverLinkOverlay from "../hover-link-overlay";
import { nip19 } from "nostr-tools";
export type NoteProps = Omit<CardProps, "children"> & { export type NoteProps = Omit<CardProps, "children"> & {
event: NostrEvent; event: NostrEvent;
variant?: CardProps["variant"]; variant?: CardProps["variant"];
showReplyButton?: boolean; showReplyButton?: boolean;
hideDrawerButton?: boolean; hideDrawerButton?: boolean;
hideThreadLink?: boolean;
registerIntersectionEntity?: boolean; registerIntersectionEntity?: boolean;
clickable?: boolean;
}; };
export const Note = React.memo( export const Note = React.memo(
({ ({
@ -59,8 +62,8 @@ export const Note = React.memo(
variant = "outline", variant = "outline",
showReplyButton, showReplyButton,
hideDrawerButton, hideDrawerButton,
hideThreadLink,
registerIntersectionEntity = true, registerIntersectionEntity = true,
clickable = true,
...props ...props
}: NoteProps) => { }: NoteProps) => {
const account = useCurrentAccount(); const account = useCurrentAccount();
@ -87,29 +90,26 @@ export const Note = React.memo(
<TrustProvider event={event}> <TrustProvider event={event}>
<ExpandProvider> <ExpandProvider>
<Card <Card
as={LinkBox}
variant={variant} variant={variant}
ref={registerIntersectionEntity ? ref : undefined} ref={registerIntersectionEntity ? ref : undefined}
data-event-id={event.id} data-event-id={event.id}
{...props} {...props}
> >
{clickable && <HoverLinkOverlay as={RouterLink} to={`/n/${nip19.noteEncode(event.id)}`} />}
<CardHeader p="2"> <CardHeader p="2">
<Flex flex="1" gap="2" alignItems="center"> <Flex flex="1" gap="2" alignItems="center">
<UserAvatarLink pubkey={event.pubkey} size={["xs", "sm"]} /> <UserAvatarLink pubkey={event.pubkey} size={["xs", "sm"]} />
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" /> <UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="lg" />
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon /> <UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
<Flex grow={1} /> <Flex grow={1} />
{!hideThreadLink && (
<NoteLink noteId={event.id} whiteSpace="nowrap" color="current">
thread
</NoteLink>
)}
{showSignatureVerification && <EventVerificationIcon event={event} />} {showSignatureVerification && <EventVerificationIcon event={event} />}
{!hideDrawerButton && ( {!hideDrawerButton && (
<OpenInDrawerButton to={`/n/${getSharableEventAddress(event)}`} size="sm" variant="ghost" /> <OpenInDrawerButton to={`/n/${getSharableEventAddress(event)}`} size="sm" variant="ghost" />
)} )}
<NoteLink noteId={event.id} whiteSpace="nowrap" color="current"> <Link as={RouterLink} whiteSpace="nowrap" color="current" to={`/n/${nip19.noteEncode(event.id)}`}>
<Timestamp timestamp={event.created_at} /> <Timestamp timestamp={event.created_at} />
</NoteLink> </Link>
</Flex> </Flex>
{community && ( {community && (
<Text fontStyle="italic"> <Text fontStyle="italic">

View File

@ -7,7 +7,7 @@ import useSubject from "../../../hooks/use-subject";
import { getMatchLink } from "../../../helpers/regexp"; import { getMatchLink } from "../../../helpers/regexp";
import { LightboxProvider } from "../../lightbox-provider"; import { LightboxProvider } from "../../lightbox-provider";
import { isImageURL } from "../../../helpers/url"; import { isImageURL } from "../../../helpers/url";
import { EmbeddedImage, EmbeddedImageProps } from "../../embed-types"; import { EmbeddedImage, EmbeddedImageProps, GalleryImage } from "../../embed-types";
import { TrustProvider } from "../../../providers/trust"; import { TrustProvider } from "../../../providers/trust";
import PhotoGallery, { PhotoWithoutSize } from "../../photo-gallery"; import PhotoGallery, { PhotoWithoutSize } from "../../photo-gallery";
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer"; import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
@ -15,11 +15,11 @@ import { NostrEvent } from "../../../types/nostr-event";
import { getEventUID } from "../../../helpers/nostr/events"; import { getEventUID } from "../../../helpers/nostr/events";
import { useBreakpointValue } from "../../../providers/breakpoint-provider"; import { useBreakpointValue } from "../../../providers/breakpoint-provider";
function GalleryImage({ event, ...props }: EmbeddedImageProps & { event: NostrEvent }) { function CustomGalleryImage({ event, ...props }: EmbeddedImageProps & { event: NostrEvent }) {
const ref = useRef<HTMLImageElement | null>(null); const ref = useRef<HTMLImageElement | null>(null);
useRegisterIntersectionEntity(ref, getEventUID(event)); useRegisterIntersectionEntity(ref, getEventUID(event));
return <EmbeddedImage {...props} event={event} ref={ref} />; return <GalleryImage {...props} event={event} ref={ref} />;
} }
type PhotoWithEvent = PhotoWithoutSize & { event: NostrEvent }; type PhotoWithEvent = PhotoWithoutSize & { event: NostrEvent };
@ -30,7 +30,9 @@ function ImageGallery({ images }: { images: PhotoWithEvent[] }) {
<PhotoGallery<Photo & { event: NostrEvent }> <PhotoGallery<Photo & { event: NostrEvent }>
layout="masonry" layout="masonry"
photos={images} photos={images}
renderPhoto={({ photo, imageProps }) => <GalleryImage event={photo.event} {...imageProps} />} renderPhoto={({ photo, imageProps }) => (
<CustomGalleryImage src={imageProps.src} event={photo.event} style={imageProps.style} />
)}
columns={rowMultiplier} columns={rowMultiplier}
/> />
); );

View File

@ -33,8 +33,8 @@ export default function buildTheme(
semanticTokens: { semanticTokens: {
colors: { colors: {
"card-hover-overlay": { "card-hover-overlay": {
_light: "blackAlpha.100", _light: "blackAlpha.50",
_dark: "whiteAlpha.100", _dark: "whiteAlpha.50",
}, },
}, },
}, },

View File

@ -47,8 +47,8 @@ export const ThreadPost = ({ post, initShowReplies, focusId }: ThreadItemProps)
<Note <Note
event={post.event} event={post.event}
borderColor={focusId === post.event.id ? "blue.500" : undefined} borderColor={focusId === post.event.id ? "blue.500" : undefined}
clickable={focusId !== post.event.id}
hideDrawerButton hideDrawerButton
hideThreadLink
/> />
</TrustProvider> </TrustProvider>
)} )}

View File

@ -53,13 +53,13 @@ export default function NoteView() {
pageContent = ( pageContent = (
<> <>
{parentPosts.map((parent) => ( {parentPosts.map((parent) => (
<Note key={parent.event.id + "-rely"} event={parent.event} hideDrawerButton hideThreadLink /> <Note key={parent.event.id + "-rely"} event={parent.event} hideDrawerButton />
))} ))}
<ThreadPost key={post.event.id} post={post} initShowReplies focusId={focusId} /> <ThreadPost key={post.event.id} post={post} initShowReplies focusId={focusId} />
</> </>
); );
} else if (events[focusId]) { } else if (events[focusId]) {
pageContent = <Note event={events[focusId]} variant="filled" hideDrawerButton hideThreadLink />; pageContent = <Note event={events[focusId]} variant="filled" hideDrawerButton />;
} }
return <VerticalPageLayout>{pageContent}</VerticalPageLayout>; return <VerticalPageLayout>{pageContent}</VerticalPageLayout>;