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;