mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-10 21:00:17 +02:00
make notes clickable
This commit is contained in:
parent
07d5e7719e
commit
d2f307642a
5
.changeset/nice-hornets-own.md
Normal file
5
.changeset/nice-hornets-own.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"nostrudel": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Make notes clickable
|
@ -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);
|
||||||
|
@ -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"] }} />;
|
||||||
}
|
}
|
||||||
|
10
src/components/hover-link-overlay.tsx
Normal file
10
src/components/hover-link-overlay.tsx
Normal 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;
|
@ -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">
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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>;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user