(null);
@@ -330,7 +90,7 @@ export const NoteContents = React.memo(({ event, trusted, maxHeight }: NoteConte
onLoad={() => testHeight()}
>
- {parts.map((part, i) => (
+ {content.map((part, i) => (
{part}
))}
diff --git a/src/components/note/note-menu.tsx b/src/components/note/note-menu.tsx
index 3cec9d7f7..d25b288fc 100644
--- a/src/components/note/note-menu.tsx
+++ b/src/components/note/note-menu.tsx
@@ -1,12 +1,17 @@
import {
+ Button,
+ Input,
MenuItem,
- useDisclosure,
Modal,
- ModalOverlay,
- ModalContent,
- ModalHeader,
ModalBody,
ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Toast,
+ useDisclosure,
+ useToast,
} from "@chakra-ui/react";
import { useCopyToClipboard } from "react-use";
import { nip19 } from "nostr-tools";
@@ -15,12 +20,18 @@ import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
import { NostrEvent } from "../../types/nostr-event";
import { MenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
-import { ClipboardIcon, CodeIcon, LikeIcon, RepostIcon } from "../icons";
-import { getReferences } from "../../helpers/nostr-event";
+import { ClipboardIcon, CodeIcon, LikeIcon, RepostIcon, TrashIcon } from "../icons";
import NoteReactionsModal from "./note-zaps-modal";
import { getEventRelays } from "../../services/event-relays";
import relayScoreboardService from "../../services/relay-scoreboard";
import NoteDebugModal from "../debug-modals/note-debug-modal";
+import { useCurrentAccount } from "../../hooks/use-current-account";
+import { useCallback, useState } from "react";
+import QuoteNote from "./quote-note";
+import { buildDeleteEvent } from "../../helpers/nostr-event";
+import signingService from "../../services/signing";
+import { nostrPostAction } from "../../classes/nostr-post-action";
+import clientRelaysService from "../../services/client-relays";
function getShareLink(eventId: string) {
const relays = getEventRelays(eventId).value;
@@ -33,11 +44,37 @@ function getShareLink(eventId: string) {
}
export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit) => {
+ const account = useCurrentAccount();
+ const toast = useToast();
const infoModal = useDisclosure();
const reactionsModal = useDisclosure();
+ const deleteModal = useDisclosure();
+ const [reason, setReason] = useState("");
+ const [deleting, setDeleting] = useState(false);
+
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
const noteId = normalizeToBech32(event.id, Bech32Prefix.Note);
+ const deleteNote = useCallback(async () => {
+ try {
+ setDeleting(true);
+ const deleteEvent = buildDeleteEvent([event.id], reason);
+ const signed = await signingService.requestSignature(deleteEvent, account);
+ const results = nostrPostAction(clientRelaysService.getWriteUrls(), signed);
+ await results.onComplete;
+ deleteModal.onClose();
+ } catch (e) {
+ if (e instanceof Error) {
+ toast({
+ status: "error",
+ description: e.message,
+ });
+ }
+ } finally {
+ setDeleting(false);
+ }
+ }, [event]);
+
return (
<>
@@ -52,16 +89,54 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit
)}
+ {account.pubkey === event.pubkey && (
+ } color="red.500" onClick={deleteModal.onOpen}>
+ Delete Note
+
+ )}
}>
View Raw
+
{infoModal.isOpen && (
)}
+
{reactionsModal.isOpen && (
)}
+
+ {deleteModal.isOpen && (
+
+
+
+
+ Delete Note?
+
+
+
+
+ setReason(e.target.value)}
+ placeholder="Reason (optional)"
+ mt="2"
+ />
+
+
+
+
+
+
+
+
+ )}
>
);
};
diff --git a/src/components/page/index.tsx b/src/components/page/index.tsx
index 73098e62b..131895665 100644
--- a/src/components/page/index.tsx
+++ b/src/components/page/index.tsx
@@ -1,24 +1,13 @@
import React from "react";
-import { Container, Flex, Heading, VStack } from "@chakra-ui/react";
+import { Container, Flex } from "@chakra-ui/react";
import { ErrorBoundary } from "../error-boundary";
import { useIsMobile } from "../../hooks/use-is-mobile";
-import { FollowingList } from "../following-list";
import { ReloadPrompt } from "../reload-prompt";
import { PostModalProvider } from "../../providers/post-modal-provider";
-import MobileHeader from "./mobile-header";
import DesktopSideNav from "./desktop-side-nav";
import MobileBottomNav from "./mobile-bottom-nav";
-const FollowingSideNav = () => {
- return (
-
- Following
-
-
- );
-};
-
export const Page = ({ children }: { children: React.ReactNode }) => {
const isMobile = useIsMobile();
@@ -34,13 +23,11 @@ export const Page = ({ children }: { children: React.ReactNode }) => {
padding="0"
>
- {isMobile && }
{!isMobile && }
{children}
- {!isMobile && }
{isMobile && }
diff --git a/src/components/page/mobile-bottom-nav.tsx b/src/components/page/mobile-bottom-nav.tsx
index bafc46300..9c8cbdd53 100644
--- a/src/components/page/mobile-bottom-nav.tsx
+++ b/src/components/page/mobile-bottom-nav.tsx
@@ -1,43 +1,53 @@
-import { Flex, IconButton } from "@chakra-ui/react";
-import { useContext } from "react";
-import { useNavigate } from "react-router-dom";
+import { Flex, IconButton, useDisclosure } from "@chakra-ui/react";
+import { useContext, useEffect } from "react";
+import { useLocation, useNavigate } from "react-router-dom";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { PostModalContext } from "../../providers/post-modal-provider";
import { ChatIcon, HomeIcon, NotificationIcon, PlusCircleIcon, SearchIcon } from "../icons";
+import { UserAvatar } from "../user-avatar";
+import MobileSideDrawer from "./mobile-side-drawer";
export default function MobileBottomNav() {
+ const { isOpen, onOpen, onClose } = useDisclosure();
const { openModal } = useContext(PostModalContext);
const navigate = useNavigate();
const account = useCurrentAccount();
+ const location = useLocation();
+ useEffect(() => onClose(), [location.key, account]);
+
return (
-
- } aria-label="Home" onClick={() => navigate("/")} flexGrow="1" size="md" />
- }
- aria-label="Search"
- onClick={() => navigate(`/search`)}
- flexGrow="1"
- size="md"
- />
- }
- aria-label="New Note"
- onClick={() => {
- openModal();
- }}
- variant="solid"
- colorScheme="brand"
- isDisabled={account.readonly}
- />
- } aria-label="Messages" onClick={() => navigate(`/dm`)} flexGrow="1" size="md" />
- }
- aria-label="Notifications"
- onClick={() => navigate("/notifications")}
- flexGrow="1"
- size="md"
- />
-
+ <>
+
+
+ } aria-label="Home" onClick={() => navigate("/")} flexGrow="1" size="md" />
+ }
+ aria-label="Search"
+ onClick={() => navigate(`/search`)}
+ flexGrow="1"
+ size="md"
+ />
+ }
+ aria-label="New Note"
+ onClick={() => {
+ openModal();
+ }}
+ variant="solid"
+ colorScheme="brand"
+ isDisabled={account.readonly}
+ />
+ } aria-label="Messages" onClick={() => navigate(`/dm`)} flexGrow="1" size="md" />
+ }
+ aria-label="Notifications"
+ onClick={() => navigate("/notifications")}
+ flexGrow="1"
+ size="md"
+ />
+
+
+ >
);
}
diff --git a/src/components/page/mobile-header.tsx b/src/components/page/mobile-header.tsx
deleted file mode 100644
index fa323eb22..000000000
--- a/src/components/page/mobile-header.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Flex, IconButton, Text, useDisclosure } from "@chakra-ui/react";
-import { useEffect } from "react";
-import { Link, useLocation } from "react-router-dom";
-import { useCurrentAccount } from "../../hooks/use-current-account";
-import { ConnectedRelays } from "../connected-relays";
-import { NotificationIcon } from "../icons";
-import { UserAvatar } from "../user-avatar";
-import MobileSideDrawer from "./mobile-side-drawer";
-
-export default function MobileHeader() {
- const { isOpen, onOpen, onClose } = useDisclosure();
- const account = useCurrentAccount();
-
- const location = useLocation();
- useEffect(() => onClose(), [location.key, account]);
-
- return (
- <>
-
-
- {account.readonly && (
-
- Readonly Mode
-
- )}
-
-
- >
- );
-}
diff --git a/src/components/repost-note.tsx b/src/components/repost-note.tsx
index 98a8ad8c4..4b6e8032f 100644
--- a/src/components/repost-note.tsx
+++ b/src/components/repost-note.tsx
@@ -9,7 +9,7 @@ import { NoteMenu } from "./note/note-menu";
import { UserAvatar } from "./user-avatar";
import { UserDnsIdentityIcon } from "./user-dns-identity";
import { UserLink } from "./user-link";
-import { getUserDisplayName } from "../helpers/user-metadata";
+import { unique } from "../helpers/array";
export default function RepostNote({ event, maxHeight }: { event: NostrEvent; maxHeight?: number }) {
const {
@@ -19,7 +19,9 @@ export default function RepostNote({ event, maxHeight }: { event: NostrEvent; ma
} = useAsync(async () => {
const [_, eventId, relay] = event.tags.find(isETag) ?? [];
if (eventId) {
- return singleEventService.requestEvent(eventId, relay ? [relay] : clientRelaysService.getReadUrls());
+ const readRelays = clientRelaysService.getReadUrls();
+ if (relay) readRelays.push(relay);
+ return singleEventService.requestEvent(eventId, unique(readRelays));
}
return null;
}, [event]);
diff --git a/src/helpers/embeds.ts b/src/helpers/embeds.ts
new file mode 100644
index 000000000..601964f38
--- /dev/null
+++ b/src/helpers/embeds.ts
@@ -0,0 +1,32 @@
+import { cloneElement } from "react";
+
+export type EmbedableContent = (string | JSX.Element)[];
+export type EmbedType = {
+ regexp: RegExp;
+ render: (match: RegExpMatchArray) => JSX.Element | string;
+ name: string;
+};
+
+export function embedJSX(content: EmbedableContent, embed: EmbedType): EmbedableContent {
+ return content
+ .map((subContent, i) => {
+ if (typeof subContent === "string") {
+ const match = subContent.match(embed.regexp);
+
+ if (match && match.index !== undefined) {
+ const before = subContent.slice(0, match.index);
+ const after = subContent.slice(match.index + match[0].length, subContent.length);
+ let embedRender = embed.render(match);
+
+ if (typeof embedRender !== "string" && !embedRender.props.key) {
+ embedRender = cloneElement(embedRender, { key: embed.name + i });
+ }
+
+ return [...embedJSX([before], embed), embedRender, ...embedJSX([after], embed)];
+ }
+ }
+
+ return subContent;
+ })
+ .flat();
+}
diff --git a/src/helpers/nostr-event.ts b/src/helpers/nostr-event.ts
index 24dd2fa89..b373e7575 100644
--- a/src/helpers/nostr-event.ts
+++ b/src/helpers/nostr-event.ts
@@ -143,6 +143,15 @@ export function buildQuoteRepost(event: NostrEvent): DraftNostrEvent {
};
}
+export function buildDeleteEvent(eventIds: string[], reason = ""): DraftNostrEvent {
+ return {
+ kind: Kind.EventDeletion,
+ tags: eventIds.map((id) => ["e", id]),
+ content: reason,
+ created_at: moment().unix(),
+ };
+}
+
export function parseRTag(tag: RTag): RelayConfig {
switch (tag[2]) {
case "write":
diff --git a/src/views/dm/chat.tsx b/src/views/dm/chat.tsx
index de90ab94f..ee6e8890d 100644
--- a/src/views/dm/chat.tsx
+++ b/src/views/dm/chat.tsx
@@ -1,4 +1,4 @@
-import { Button, Card, CardBody, CardProps, Flex, IconButton, Spacer, Text, Textarea } from "@chakra-ui/react";
+import { Box, Button, Card, CardBody, CardProps, Flex, IconButton, Spacer, Text, Textarea } from "@chakra-ui/react";
import moment from "moment";
import { Kind } from "nostr-tools";
import { useEffect, useMemo, useState } from "react";
@@ -17,6 +17,20 @@ import clientRelaysService from "../../services/client-relays";
import directMessagesService, { getMessageRecipient } from "../../services/direct-messages";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import DecryptPlaceholder from "./decrypt-placeholder";
+import { EmbedableContent } from "../../helpers/embeds";
+import { embedImages, embedLinks, embedNostrLinks, embedVideos } from "../../components/embed-types";
+
+function MessageContent({ event, text }: { event: NostrEvent; text: string }) {
+ let content: EmbedableContent = [text];
+
+ content = embedImages(content, true);
+ content = embedVideos(content);
+ content = embedLinks(content);
+
+ content = embedNostrLinks(content, event);
+
+ return {content};
+}
function Message({ event }: { event: NostrEvent } & Omit) {
const account = useCurrentAccount();
@@ -33,7 +47,7 @@ function Message({ event }: { event: NostrEvent } & Omit)
data={event.content}
pubkey={isOwnMessage ? getMessageRecipient(event) ?? "" : event.pubkey}
>
- {(text) => {text}}
+ {(text) => }
diff --git a/src/views/dm/index.tsx b/src/views/dm/index.tsx
index 0bd44bf43..534ab6fde 100644
--- a/src/views/dm/index.tsx
+++ b/src/views/dm/index.tsx
@@ -8,13 +8,14 @@ import {
Card,
CardBody,
Flex,
+ Link,
LinkBox,
LinkOverlay,
Text,
} from "@chakra-ui/react";
import moment from "moment";
import { useEffect, useMemo, useState } from "react";
-import { Link } from "react-router-dom";
+import { Link as RouterLink } from "react-router-dom";
import { UserAvatar } from "../../components/user-avatar";
import { convertTimestampToDate } from "../../helpers/date";
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
@@ -22,6 +23,8 @@ import { getUserDisplayName } from "../../helpers/user-metadata";
import useSubject from "../../hooks/use-subject";
import { useUserMetadata } from "../../hooks/use-user-metadata";
import directMessagesService from "../../services/direct-messages";
+import { ExternalLinkIcon } from "../../components/icons";
+import { useIsMobile } from "../../hooks/use-is-mobile";
function ContactCard({ pubkey }: { pubkey: string }) {
const subject = useMemo(() => directMessagesService.getUserMessages(pubkey), [pubkey]);
@@ -40,12 +43,13 @@ function ContactCard({ pubkey }: { pubkey: string }) {
)}