) {
+ const content = useRenderedContent(post, components, {
+ linkRenderers,
+ transformers,
+ cacheKey: MediaPostContentSymbol,
+ });
+
+ return (
+
+ {content}
+
+ );
+}
diff --git a/src/components/media-post/media-slides.tsx b/src/components/media-post/media-slides.tsx
new file mode 100644
index 000000000..6b5ed33e0
--- /dev/null
+++ b/src/components/media-post/media-slides.tsx
@@ -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 (
+
+ }
+ onClick={goBack}
+ aria-label="previous image"
+ variant="ghost"
+ h="24"
+ w="12"
+ isDisabled={!enablePrevNavButton}
+ >
+ PREV
+
+ }
+ onClick={goForward}
+ aria-label="next image"
+ variant="ghost"
+ h="24"
+ w="12"
+ isDisabled={!enableNextNavButton}
+ >
+ NEXT
+
+
+ );
+}
+
+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 (
+
+ {[...Array(totalPages)].map((_, index) => (
+
+ ))}
+
+ );
+}
+
+function MediaAttachmentSlide({ media }: { media: MediaAttachment }) {
+ if (media.type?.startsWith("video/") || isVideoURL(media.url)) {
+ return ;
+ } else if (media.type?.startsWith("image/") || isImageURL(media.url)) {
+ return ;
+ }
+
+ return (
+
+ Unknown media type {media.type ?? "Unknown"}
+
+ );
+}
+
+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) {
+ const attachments = getMediaAttachments(post);
+
+ if (attachments.length === 1)
+ return (
+
+
+
+
+ {showZaps && }
+
+ );
+
+ return (
+
+ }
+ showArrows
+ dots={
+
+ {showZaps && }
+
+
+
+ }
+ >
+ {attachments.map((media) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/note/note-zap-button.tsx b/src/components/note/note-zap-button.tsx
index 0d9ece19a..6381077cf 100644
--- a/src/components/note/note-zap-button.tsx
+++ b/src/components/note/note-zap-button.tsx
@@ -55,6 +55,7 @@ export default function NoteZapButton({ event, allowComment, showEventPreview, .
icon={}
aria-label="Zap Note"
title="Zap Note"
+ colorScheme={hasZapped ? "primary" : undefined}
{...props}
onClick={onOpen}
isDisabled={!canZap}
diff --git a/src/components/timeline-page/generic-note-timeline/timeline-item.tsx b/src/components/timeline-page/generic-note-timeline/timeline-item.tsx
index dd18ec7af..126c67c96 100644
--- a/src/components/timeline-page/generic-note-timeline/timeline-item.tsx
+++ b/src/components/timeline-page/generic-note-timeline/timeline-item.tsx
@@ -1,6 +1,6 @@
import { ReactNode, memo } from "react";
import { kinds } from "nostr-tools";
-import { Box, Text } from "@chakra-ui/react";
+import { Box } from "@chakra-ui/react";
import { ErrorBoundary } from "../../error-boundary";
import ReplyNote from "./reply-note";
@@ -16,6 +16,8 @@ import { TimelineNote } from "../../note/timeline-note";
import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref";
import ArticleCard from "../../../views/articles/components/article-card";
import EmbeddedUnknown from "../../embed-event/event-types/embedded-unknown";
+import { MEDIA_POST_KIND } from "../../../helpers/nostr/media";
+import MediaPost from "../../media-post/media-post-card";
function TimelineItem({ event, visible, minHeight }: { event: NostrEvent; visible: boolean; minHeight?: number }) {
const ref = useEventIntersectionRef(event);
@@ -44,6 +46,9 @@ function TimelineItem({ event, visible, minHeight }: { event: NostrEvent; visibl
case kinds.LongFormArticle:
content = ;
break;
+ case MEDIA_POST_KIND:
+ content = ;
+ break;
default:
content = ;
break;
diff --git a/src/components/zap/event-zap-icon-button.tsx b/src/components/zap/event-zap-icon-button.tsx
new file mode 100644
index 000000000..b3496194a
--- /dev/null
+++ b/src/components/zap/event-zap-icon-button.tsx
@@ -0,0 +1,43 @@
+import { IconButton, IconButtonProps, useDisclosure } from "@chakra-ui/react";
+import { getZapSender } from "applesauce-core/helpers";
+
+import useCurrentAccount from "../../hooks/use-current-account";
+import useEventZaps from "../../hooks/use-event-zaps";
+import eventZapsService from "../../services/event-zaps";
+import { NostrEvent } from "../../types/nostr-event";
+import { LightningIcon } from "../icons";
+import ZapModal from "../event-zap-modal";
+import useUserLNURLMetadata from "../../hooks/use-user-lnurl-metadata";
+import { getEventUID } from "../../helpers/nostr/event";
+import { useReadRelays } from "../../hooks/use-client-relays";
+
+export default function EventZapIconButton({
+ event,
+ ...props
+}: { event: NostrEvent } & Omit) {
+ const account = useCurrentAccount();
+ const { metadata } = useUserLNURLMetadata(event.pubkey);
+ const zaps = useEventZaps(getEventUID(event)) ?? [];
+ const { isOpen, onOpen, onClose } = useDisclosure();
+
+ const readRelays = useReadRelays();
+ const onZapped = () => {
+ onClose();
+ eventZapsService.requestZaps(getEventUID(event), readRelays, true);
+ };
+
+ const canZap = !!metadata?.allowsNostr || event.tags.some((t) => t[0] === "zap");
+
+ return (
+ <>
+ }
+ {...props}
+ onClick={onOpen}
+ isDisabled={!canZap}
+ />
+
+ {isOpen && }
+ >
+ );
+}
diff --git a/src/const.ts b/src/const.ts
index 62afb4409..ac31d39e1 100644
--- a/src/const.ts
+++ b/src/const.ts
@@ -1,4 +1,5 @@
-import { safeRelayUrls } from "applesauce-core/helpers";
+import { getCoordinateFromAddressPointer, safeRelayUrls } from "applesauce-core/helpers";
+import { EventFactoryClient } from "applesauce-factory";
import { kinds } from "nostr-tools";
export const DEFAULT_SEARCH_RELAYS = safeRelayUrls([
@@ -47,10 +48,14 @@ export const NOSTR_CONNECT_PERMISSIONS = [
];
export const NEVER_ATTACH_CLIENT_TAG = [kinds.EncryptedDirectMessage];
-export const NIP_89_CLIENT_TAG = [
- "client",
- "noStrudel",
- "31990:266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5:1686066542546",
-];
+
+export const NIP_89_CLIENT_APP: EventFactoryClient = {
+ name: "noStrudel",
+ address: {
+ kind: kinds.Handlerinformation,
+ pubkey: "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
+ identifier: "1686066542546",
+ },
+};
export const SUPPORT_PUBKEY = "713978c3094081b34fcf2f5491733b0c22728cd3b7a6946519d40f5f08598af8";
diff --git a/src/helpers/nostr/media.ts b/src/helpers/nostr/media.ts
new file mode 100644
index 000000000..f7aa33dcd
--- /dev/null
+++ b/src/helpers/nostr/media.ts
@@ -0,0 +1 @@
+export const MEDIA_POST_KIND = 20;
diff --git a/src/helpers/nostr/post.ts b/src/helpers/nostr/post.ts
index c7d8f005a..406184a5b 100644
--- a/src/helpers/nostr/post.ts
+++ b/src/helpers/nostr/post.ts
@@ -155,6 +155,7 @@ export function ensureTagContentMentions(draft: EventTemplate) {
return updated;
}
+/** @deprecated use includeContentHashtags from applesauce-factory instead */
export function createHashtagTags(draft: EventTemplate) {
const updatedDraft: EventTemplate = { ...draft, tags: Array.from(draft.tags) };
@@ -214,6 +215,7 @@ export function addPubkeyRelayHints(draft: EventTemplate) {
};
}
+/** @deprecated use event factory instead */
export function finalizeNote(draft: EventTemplate) {
let updated: EventTemplate = { ...draft, tags: Array.from(draft.tags) };
updated.content = correctContentMentions(updated.content);
diff --git a/src/providers/global/event-factory-provider.tsx b/src/providers/global/event-factory-provider.tsx
new file mode 100644
index 000000000..73cab02cb
--- /dev/null
+++ b/src/providers/global/event-factory-provider.tsx
@@ -0,0 +1,11 @@
+import { useObservable } from "applesauce-react/hooks";
+import { FactoryProvider } from "applesauce-react/providers";
+
+import eventFactoryService from "../../services/event-factory";
+import { PropsWithChildren } from "react";
+
+export default function EventFactoryProvider({ children }: PropsWithChildren) {
+ const factory = useObservable(eventFactoryService.subject);
+
+ return {children};
+}
diff --git a/src/providers/global/index.tsx b/src/providers/global/index.tsx
index c45a77760..5b97e982a 100644
--- a/src/providers/global/index.tsx
+++ b/src/providers/global/index.tsx
@@ -1,6 +1,6 @@
import React, { useMemo } from "react";
import { ChakraProvider, localStorageManager } from "@chakra-ui/react";
-import { QueryStoreProvider } from "applesauce-react";
+import { QueryStoreProvider } from "applesauce-react/providers";
import { SigningProvider } from "./signing-provider";
import buildTheme from "../../theme";
@@ -12,6 +12,7 @@ import DMTimelineProvider from "./dms-provider";
import PublishProvider from "./publish-provider";
import WebOfTrustProvider from "./web-of-trust-provider";
import { queryStore } from "../../services/event-store";
+import EventFactoryProvider from "./event-factory-provider";
function ThemeProviders({ children }: { children: React.ReactNode }) {
const { theme: themeName, primaryColor, maxPageWidth } = useAppSettings();
@@ -33,17 +34,19 @@ export const GlobalProviders = ({ children }: { children: React.ReactNode }) =>
-
-
-
-
-
- {children}
-
-
-
-
-
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
diff --git a/src/providers/global/publish-provider.tsx b/src/providers/global/publish-provider.tsx
index 296a8ff95..3586adc6f 100644
--- a/src/providers/global/publish-provider.tsx
+++ b/src/providers/global/publish-provider.tsx
@@ -1,6 +1,7 @@
import { PropsWithChildren, createContext, useCallback, useContext, useMemo, useState } from "react";
import { useToast } from "@chakra-ui/react";
-import { EventTemplate, NostrEvent, UnsignedEvent, getEventHash, kinds } from "nostr-tools";
+import { EventTemplate, NostrEvent, UnsignedEvent, kinds } from "nostr-tools";
+import { includeClientTag } from "applesauce-factory/operations";
import { useSigningContext } from "./signing-provider";
import { DraftNostrEvent } from "../../types/nostr-event";
@@ -9,11 +10,10 @@ import clientRelaysService from "../../services/client-relays";
import RelaySet from "../../classes/relay-set";
import { cloneEvent, getAllRelayHints, isReplaceable } from "../../helpers/nostr/event";
import replaceableEventsService from "../../services/replaceable-events";
-import eventReactionsService from "../../services/event-reactions";
import { localRelay } from "../../services/local-relay";
import deleteEventService from "../../services/delete-events";
import localSettings from "../../services/local-settings";
-import { NEVER_ATTACH_CLIENT_TAG, NIP_89_CLIENT_TAG } from "../../const";
+import { NEVER_ATTACH_CLIENT_TAG, NIP_89_CLIENT_APP } from "../../const";
import { eventStore } from "../../services/event-store";
import { addPubkeyRelayHints } from "../../helpers/nostr/post";
import useCurrentAccount from "../../hooks/use-current-account";
@@ -70,19 +70,24 @@ export default function PublishProvider({ children }: PropsWithChildren) {
const outBoxes = useUserOutbox(account?.pubkey);
const finalizeDraft = useCallback(
- (event: EventTemplate | NostrEvent) => {
+ async (event: EventTemplate | NostrEvent) => {
let draft = cloneEvent(event.kind, event);
// add pubkey relay hints
draft = addPubkeyRelayHints(draft);
// add client tag
- if (localSettings.addClientTag.value && !NEVER_ATTACH_CLIENT_TAG.includes(draft.kind)) {
- draft.tags = [...draft.tags.filter((t) => t[0] !== "client"), NIP_89_CLIENT_TAG];
+ if (
+ localSettings.addClientTag.value &&
+ !NEVER_ATTACH_CLIENT_TAG.includes(draft.kind) &&
+ !draft.tags.some((t) => t[0] === "client")
+ ) {
+ // TODO: this should be removed when all events are created using the event factory
+ draft = await includeClientTag(NIP_89_CLIENT_APP.name, NIP_89_CLIENT_APP.address)(draft, {});
}
// request signature
- return signerFinalize(draft);
+ return await signerFinalize(draft);
},
[signerFinalize],
);
diff --git a/src/services/event-factory.ts b/src/services/event-factory.ts
new file mode 100644
index 000000000..e3504af4a
--- /dev/null
+++ b/src/services/event-factory.ts
@@ -0,0 +1,32 @@
+import { BehaviorSubject, Observable } from "rxjs";
+import { EventFactory } from "applesauce-factory";
+import { Account } from "../classes/accounts/account";
+import { getEventRelayHints } from "./event-relay-hint";
+import { NIP_89_CLIENT_APP } from "../const";
+import accountService from "./account";
+
+class EventFactoryService {
+ subject = new BehaviorSubject(null);
+
+ get factory() {
+ return this.subject.value;
+ }
+
+ constructor(account: Observable) {
+ account.subscribe((current) => {
+ if (!current) this.subject.next(null);
+ else
+ this.subject.next(
+ new EventFactory({
+ signer: current.signer,
+ getRelayHint: (event) => getEventRelayHints(event, 1)[0],
+ client: NIP_89_CLIENT_APP,
+ }),
+ );
+ });
+ }
+}
+
+const eventFactoryService = new EventFactoryService(accountService.current);
+
+export default eventFactoryService;
diff --git a/src/views/media/index.tsx b/src/views/media/index.tsx
new file mode 100644
index 000000000..0226ae53e
--- /dev/null
+++ b/src/views/media/index.tsx
@@ -0,0 +1,50 @@
+import { useCallback } from "react";
+import { Flex } from "@chakra-ui/react";
+import { NostrEvent } from "nostr-tools";
+
+import PeopleListProvider, { usePeopleListContext } from "../../providers/local/people-list-provider";
+import useClientSideMuteFilter from "../../hooks/use-client-side-mute-filter";
+import { useReadRelays } from "../../hooks/use-client-relays";
+import useTimelineLoader from "../../hooks/use-timeline-loader";
+import { MEDIA_POST_KIND } from "../../helpers/nostr/media";
+import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
+import TimelinePage from "../../components/timeline-page";
+
+function MediaFeedPage() {
+ const muteFilter = useClientSideMuteFilter();
+ const eventFilter = useCallback(
+ (event: NostrEvent) => {
+ if (muteFilter(event)) return false;
+ return true;
+ },
+ [muteFilter],
+ );
+
+ const relays = useReadRelays();
+ const { listId, filter } = usePeopleListContext();
+
+ const { loader, timeline } = useTimelineLoader(
+ `${listId}-media-feed`,
+ relays,
+ filter ? { ...filter, kinds: [MEDIA_POST_KIND] } : undefined,
+ {
+ eventFilter,
+ },
+ );
+
+ const header = (
+
+
+
+ );
+
+ return ;
+}
+
+export default function MediaFeedView() {
+ return (
+
+
+
+ );
+}
diff --git a/src/views/media/media-comments.tsx b/src/views/media/media-comments.tsx
new file mode 100644
index 000000000..e5674750e
--- /dev/null
+++ b/src/views/media/media-comments.tsx
@@ -0,0 +1,44 @@
+import { Box, ButtonGroup } from "@chakra-ui/react";
+import { NostrEvent } from "nostr-tools";
+import { COMMENT_KIND } from "applesauce-core/helpers";
+import { useStoreQuery } from "applesauce-react/hooks";
+import { CommentsQuery } from "applesauce-core/queries";
+
+import { useReadRelays } from "../../hooks/use-client-relays";
+import UserLink from "../../components/user/user-link";
+import DebugEventButton from "../../components/debug-modal/debug-event-button";
+import useTimelineLoader from "../../hooks/use-timeline-loader";
+import IntersectionObserverProvider from "../../providers/local/intersection-observer";
+import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
+import TextNoteContents from "../../components/note/timeline-note/text-note-contents";
+import Timestamp from "../../components/timestamp";
+
+function Comment({ comment }: { comment: NostrEvent }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function MediaPostComments({ post }: { post: NostrEvent }) {
+ const readRelays = useReadRelays();
+ const { loader } = useTimelineLoader(`${post.id}-comments`, readRelays, { kinds: [COMMENT_KIND], "#E": [post.id] });
+
+ const comments = useStoreQuery(CommentsQuery, [post]);
+ const callback = useTimelineCurserIntersectionCallback(loader);
+
+ return (
+
+ {comments?.map((comment) => )}
+
+ );
+}
diff --git a/src/views/media/media-post-comment-form.tsx b/src/views/media/media-post-comment-form.tsx
new file mode 100644
index 000000000..8b495cb1f
--- /dev/null
+++ b/src/views/media/media-post-comment-form.tsx
@@ -0,0 +1,67 @@
+import { useRef } from "react";
+import { Box, ComponentWithAs, Flex, FlexProps, IconButton, useToast } from "@chakra-ui/react";
+import { useForm } from "react-hook-form";
+import { NostrEvent } from "nostr-tools";
+import { useEventFactory } from "applesauce-react/hooks";
+
+import { usePublishEvent } from "../../providers/global/publish-provider";
+import { useContextEmojis } from "../../providers/global/emoji-provider";
+import { MagicInput, RefType } from "../../components/magic-textarea";
+import useTextAreaUploadFile, { useTextAreaInsertTextWithForm } from "../../hooks/use-textarea-upload-file";
+import { useWriteRelays } from "../../hooks/use-client-relays";
+import MessageSquare01 from "../../components/icons/message-square-01";
+
+export default function MediaPostCommentForm({
+ post,
+ ...props
+}: { post: NostrEvent } & Omit) {
+ const toast = useToast();
+ const publish = usePublishEvent();
+ const emojis = useContextEmojis();
+ const factory = useEventFactory();
+
+ const relays = useWriteRelays();
+ const { setValue, handleSubmit, formState, reset, getValues, watch } = useForm({
+ defaultValues: { content: "" },
+ });
+ const sendMessage = handleSubmit(async (values) => {
+ try {
+ if (!factory) throw new Error("Missing factory");
+ let draft = await factory.comment(post, values.content);
+ const pub = await publish("Comment", draft, relays);
+ if (pub) reset();
+ } catch (error) {
+ if (error instanceof Error) toast({ description: error.message, status: "error" });
+ }
+ });
+
+ const textAreaRef = useRef(null);
+ const insertText = useTextAreaInsertTextWithForm(textAreaRef, getValues, setValue);
+ const { onPaste } = useTextAreaUploadFile(insertText);
+
+ watch("content");
+
+ return (
+ <>
+
+ (textAreaRef.current = inst)}
+ placeholder="Comment"
+ autoComplete="off"
+ isRequired
+ value={getValues().content}
+ onChange={(e) => setValue("content", e.target.value, { shouldDirty: true })}
+ // @ts-expect-error
+ onPaste={onPaste}
+ />
+ }
+ aria-label="Comment"
+ />
+
+ >
+ );
+}
diff --git a/src/views/media/media-post.tsx b/src/views/media/media-post.tsx
new file mode 100644
index 000000000..fcaaeb003
--- /dev/null
+++ b/src/views/media/media-post.tsx
@@ -0,0 +1,120 @@
+import { Box, ButtonGroup, Flex, Heading, Spinner } from "@chakra-ui/react";
+import { NostrEvent } from "nostr-tools";
+import { COMMENT_KIND } from "applesauce-core/helpers";
+import { useStoreQuery } from "applesauce-react/hooks";
+import { CommentsQuery } from "applesauce-core/queries";
+
+import { useReadRelays } from "../../hooks/use-client-relays";
+import useParamsEventPointer from "../../hooks/use-params-event-pointer";
+import useSingleEvent from "../../hooks/use-single-event";
+import UserAvatarLink from "../../components/user/user-avatar-link";
+import UserLink from "../../components/user/user-link";
+import UserDnsIdentity from "../../components/user/user-dns-identity";
+import MediaPostSlides from "../../components/media-post/media-slides";
+import MediaPostContents from "../../components/media-post/media-post-content";
+import { TrustProvider } from "../../providers/local/trust-provider";
+import DebugEventButton from "../../components/debug-modal/debug-event-button";
+import RepostButton from "../../components/note/timeline-note/components/repost-button";
+import QuoteEventButton from "../../components/note/quote-event-button";
+import useTimelineLoader from "../../hooks/use-timeline-loader";
+import IntersectionObserverProvider from "../../providers/local/intersection-observer";
+import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
+import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
+import EventZapIconButton from "../../components/zap/event-zap-icon-button";
+import AddReactionButton from "../../components/note/timeline-note/components/add-reaction-button";
+import EventReactionButtons from "../../components/event-reactions/event-reactions";
+import { MediaPostComments } from "./media-comments";
+import MediaPostCommentForm from "./media-post-comment-form";
+
+function Header({ post }: { post: NostrEvent }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function Actions({ post }: { post: NostrEvent }) {
+ return (
+
+
+
+
+
+ );
+}
+
+function HorizontalLayout({ post }: { post: NostrEvent }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Comments:
+
+
+
+
+
+
+ );
+}
+
+function VerticalLayout({ post }: { post: NostrEvent }) {
+ return (
+
+
+
+
+
+
+
+
+
+ Comments:
+
+
+
+
+ );
+}
+
+function MediaPostPage({ post }: { post: NostrEvent }) {
+ const Layout = useBreakpointValue({ base: VerticalLayout, lg: HorizontalLayout }) || VerticalLayout;
+
+ return (
+
+
+
+ );
+}
+
+export default function MediaPostView() {
+ const pointer = useParamsEventPointer("pointer");
+ const readRelays = useReadRelays(pointer.relays);
+
+ const post = useSingleEvent(pointer.id, readRelays);
+
+ if (post) return ;
+ else return ;
+}
diff --git a/src/views/other-stuff/apps.ts b/src/views/other-stuff/apps.ts
index 42a5f3366..c59e1ae0e 100644
--- a/src/views/other-stuff/apps.ts
+++ b/src/views/other-stuff/apps.ts
@@ -23,6 +23,7 @@ import MessageQuestionSquare from "../../components/icons/message-question-squar
import UploadCloud01 from "../../components/icons/upload-cloud-01";
import Edit04 from "../../components/icons/edit-04";
import Users03 from "../../components/icons/users-03";
+import Camera01 from "../../components/icons/camera-01";
export const internalApps: App[] = [
{
@@ -32,6 +33,13 @@ export const internalApps: App[] = [
id: "streams",
to: "/streams",
},
+ {
+ title: "Media",
+ description: "Browser media posts",
+ icon: Camera01,
+ id: "media",
+ to: "/media",
+ },
{
title: "Communities",
description: "Create and manage communities",
diff --git a/src/views/user/about/user-recent-events.tsx b/src/views/user/about/user-recent-events.tsx
index 993fe987f..bc910d98c 100644
--- a/src/views/user/about/user-recent-events.tsx
+++ b/src/views/user/about/user-recent-events.tsx
@@ -22,6 +22,8 @@ import { useUserOutbox } from "../../../hooks/use-user-mailboxes";
import { useReadRelays } from "../../../hooks/use-client-relays";
import AlertTriangle from "../../../components/icons/alert-triangle";
import MessageSquare02 from "../../../components/icons/message-square-02";
+import Camera01 from "../../../components/icons/camera-01";
+import { MEDIA_POST_KIND } from "../../../helpers/nostr/media";
type KnownKind = {
kind: number;
@@ -75,6 +77,13 @@ const KnownKinds: KnownKind[] = [
link: (_, p) => `/u/${npubEncode(p)}/articles`,
},
+ {
+ kind: MEDIA_POST_KIND,
+ name: "Media",
+ icon: Camera01,
+ link: (_, p) => `/u/${npubEncode(p)}/media`,
+ },
+
{
kind: kinds.EncryptedDirectMessage,
name: "Legacy DMs",
diff --git a/src/views/user/index.tsx b/src/views/user/index.tsx
index e8063e2f1..2fbbfbe22 100644
--- a/src/views/user/index.tsx
+++ b/src/views/user/index.tsx
@@ -45,6 +45,7 @@ const tabs = [
{ label: "Notes", path: "notes" },
{ label: "Articles", path: "articles" },
{ label: "Streams", path: "streams" },
+ { label: "Media", path: "media" },
{ label: "Zaps", path: "zaps" },
{ label: "Lists", path: "lists" },
{ label: "Following", path: "following" },
diff --git a/src/views/user/media-posts.tsx b/src/views/user/media-posts.tsx
new file mode 100644
index 000000000..650d89b2c
--- /dev/null
+++ b/src/views/user/media-posts.tsx
@@ -0,0 +1,18 @@
+import { useOutletContext } from "react-router-dom";
+
+import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
+import useTimelineLoader from "../../hooks/use-timeline-loader";
+import TimelinePage from "../../components/timeline-page";
+import { MEDIA_POST_KIND } from "../../helpers/nostr/media";
+
+export default function UserMediaPostsTab() {
+ const { pubkey } = useOutletContext() as { pubkey: string };
+ const readRelays = useAdditionalRelayContext();
+
+ const { loader, timeline } = useTimelineLoader(pubkey + "-media-posts", readRelays, {
+ authors: [pubkey],
+ kinds: [MEDIA_POST_KIND],
+ });
+
+ return ;
+}