diff --git a/.changeset/strange-turtles-love.md b/.changeset/strange-turtles-love.md new file mode 100644 index 000000000..7bdacb4d1 --- /dev/null +++ b/.changeset/strange-turtles-love.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Add media tab in user view diff --git a/src/app.tsx b/src/app.tsx index f63f72ab6..9ddde7df5 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -34,6 +34,7 @@ import DirectMessageChatView from "./views/dm/chat"; import NostrLinkView from "./views/link"; import UserReportsTab from "./views/user/reports"; import appSettings from "./services/app-settings"; +import UserMediaTab from "./views/user/media"; // code split search view because QrScanner library is 400kB const SearchView = React.lazy(() => import("./views/search")); @@ -91,6 +92,7 @@ const router = createBrowserRouter([ children: [ { path: "", element: }, { path: "notes", element: }, + { path: "media", element: }, { path: "zaps", element: }, { path: "followers", element: }, { path: "following", element: }, diff --git a/src/components/debug-modals/note-debug-modal.tsx b/src/components/debug-modals/note-debug-modal.tsx index fe76510ca..8ea43a4c5 100644 --- a/src/components/debug-modals/note-debug-modal.tsx +++ b/src/components/debug-modals/note-debug-modal.tsx @@ -3,8 +3,9 @@ import { ModalProps } from "@chakra-ui/react"; import { Bech32Prefix, hexToBech32 } from "../../helpers/nip19"; import { getReferences } from "../../helpers/nostr-event"; import { NostrEvent } from "../../types/nostr-event"; -import RawJson from "./raw-block"; +import RawJson from "./raw-json"; import RawValue from "./raw-value"; +import RawPre from "./raw-pre"; export default function NoteDebugModal({ event, ...props }: { event: NostrEvent } & Omit) { return ( @@ -16,7 +17,8 @@ export default function NoteDebugModal({ event, ...props }: { event: NostrEvent - + + diff --git a/src/components/debug-modals/raw-block.tsx b/src/components/debug-modals/raw-json.tsx similarity index 100% rename from src/components/debug-modals/raw-block.tsx rename to src/components/debug-modals/raw-json.tsx diff --git a/src/components/debug-modals/raw-pre.tsx b/src/components/debug-modals/raw-pre.tsx new file mode 100644 index 000000000..2fcc824f1 --- /dev/null +++ b/src/components/debug-modals/raw-pre.tsx @@ -0,0 +1,16 @@ +import { Box, Code, Flex, Heading } from "@chakra-ui/react"; + +export default function RawPre({ value, heading }: { heading: string; value: string }) { + return ( + + + {heading} + + + + {value} + + + + ); +} diff --git a/src/components/debug-modals/user-debug-modal.tsx b/src/components/debug-modals/user-debug-modal.tsx index a1c204851..add5e1f92 100644 --- a/src/components/debug-modals/user-debug-modal.tsx +++ b/src/components/debug-modals/user-debug-modal.tsx @@ -4,7 +4,7 @@ import { ModalProps } from "@chakra-ui/react"; import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19"; import { useUserMetadata } from "../../hooks/use-user-metadata"; import RawValue from "./raw-value"; -import RawJson from "./raw-block"; +import RawJson from "./raw-json"; export default function UserDebugModal({ pubkey, ...props }: { pubkey: string } & Omit) { const npub = useMemo(() => normalizeToBech32(pubkey, Bech32Prefix.Pubkey), [pubkey]); diff --git a/src/components/embed-types/common.tsx b/src/components/embed-types/common.tsx index bd35fb38a..83baa2109 100644 --- a/src/components/embed-types/common.tsx +++ b/src/components/embed-types/common.tsx @@ -3,6 +3,7 @@ import { EmbedableContent, embedJSX } from "../../helpers/embeds"; import appSettings from "../../services/app-settings"; import { ImageGalleryLink } from "../image-gallery"; import { useIsMobile } from "../../hooks/use-is-mobile"; +import { matchImageUrls } from "../../helpers/regexp"; const BlurredImage = (props: ImageProps) => { const { isOpen, onOpen } = useDisclosure(); @@ -30,8 +31,7 @@ const EmbeddedImage = ({ src, blue }: { src: string; blue: boolean }) => { // note1n06jceulg3gukw836ghd94p0ppwaz6u3mksnnz960d8vlcp2fnqsgx3fu9 export function embedImages(content: EmbedableContent, trusted = false) { return embedJSX(content, { - regexp: - /https?:\/\/([\dA-z\.-]+\.[A-z\.]{2,6})((?:\/[\+~%\/\.\w\-_]*)?\.(?:svg|gif|png|jpg|jpeg|webp|avif))(\??(?:[\?#\-\+=&;%@\.\w_]*)#?(?:[\-\.\!\/\\\w]*))?/i, + regexp: matchImageUrls, render: (match) => , name: "Image", }); diff --git a/src/helpers/regexp.ts b/src/helpers/regexp.ts index 2b5611fe9..e269a039a 100644 --- a/src/helpers/regexp.ts +++ b/src/helpers/regexp.ts @@ -1 +1,3 @@ export const mentionNpubOrNote = /@?((npub1|note1)[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{58})/gi; +export const matchImageUrls = + /https?:\/\/([\dA-z\.-]+\.[A-z\.]{2,6})((?:\/[\+~%\/\.\w\-_]*)?\.(?:svg|gif|png|jpg|jpeg|webp|avif))(\??(?:[\?#\-\+=&;%@\.\w_]*)#?(?:[\-\.\!\/\\\w]*))?/i; diff --git a/src/views/user/index.tsx b/src/views/user/index.tsx index 20ce32241..9eb5017a5 100644 --- a/src/views/user/index.tsx +++ b/src/views/user/index.tsx @@ -43,6 +43,7 @@ import { RelayFavicon } from "../../components/relay-favicon"; const tabs = [ { label: "Notes", path: "notes" }, + { label: "Media", path: "media" }, { label: "Zaps", path: "zaps" }, { label: "Followers", path: "followers" }, { label: "Following", path: "following" }, diff --git a/src/views/user/media.tsx b/src/views/user/media.tsx new file mode 100644 index 000000000..398bb14f5 --- /dev/null +++ b/src/views/user/media.tsx @@ -0,0 +1,75 @@ +import { AspectRatio, Box, Button, Flex, Grid, IconButton, Image, Spinner } from "@chakra-ui/react"; +import moment from "moment"; +import { Link as RouterLink, useOutletContext } from "react-router-dom"; +import { truncatedId } from "../../helpers/nostr-event"; +import { useTimelineLoader } from "../../hooks/use-timeline-loader"; +import { useAdditionalRelayContext } from "../../providers/additional-relay-context"; +import { useMemo } from "react"; +import { matchImageUrls } from "../../helpers/regexp"; +import { useIsMobile } from "../../hooks/use-is-mobile"; +import { ImageGalleryLink, ImageGalleryProvider } from "../../components/image-gallery"; +import { ExternalLinkIcon } from "../../components/icons"; +import { getSharableNoteId } from "../../helpers/nip19"; + +const matchAllImages = new RegExp(matchImageUrls, "ig"); + +const UserMediaTab = () => { + const isMobile = useIsMobile(); + const { pubkey } = useOutletContext() as { pubkey: string }; + const contextRelays = useAdditionalRelayContext(); + + // TODO: move this out of a hook so its not being re-created every time + const { events, loading, loadMore } = useTimelineLoader( + `${truncatedId(pubkey)}-media`, + contextRelays, + { authors: [pubkey], kinds: [1] }, + { pageSize: moment.duration(1, "week").asSeconds(), startLimit: 40 } + ); + + const images = useMemo(() => { + var images: { eventId: string; src: string; index: number }[] = []; + + for (const event of events) { + const urls = event.content.matchAll(matchAllImages); + + let i = 0; + for (const url of urls) { + images.push({ eventId: event.id, src: url[0], index: i++ }); + } + } + + return images; + }, [events]); + + return ( + + + + {images.map((image) => ( + + + } + aria-label="Open note" + to={`/n/${getSharableNoteId(image.eventId)}`} + position="absolute" + right="2" + top="2" + size="sm" + /> + + ))} + + + {loading ? : } + + ); +}; + +export default UserMediaTab;