diff --git a/.changeset/flat-scissors-do.md b/.changeset/flat-scissors-do.md
new file mode 100644
index 000000000..304bc7dba
--- /dev/null
+++ b/.changeset/flat-scissors-do.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Add goal views
diff --git a/.changeset/silent-wombats-flow.md b/.changeset/silent-wombats-flow.md
new file mode 100644
index 000000000..e5c363293
--- /dev/null
+++ b/.changeset/silent-wombats-flow.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Improve event embed card
diff --git a/src/app.tsx b/src/app.tsx
index e1700c0cb..8756f479d 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -37,14 +37,18 @@ import RelaysView from "./views/relays";
import RelayView from "./views/relays/relay";
import RelayReviewsView from "./views/relays/reviews";
import ListsView from "./views/lists";
-import ListView from "./views/lists/list";
+import ListDetailsView from "./views/lists/list-details";
import UserListsTab from "./views/user/lists";
import BrowseListView from "./views/lists/browse";
import EmojiPacksBrowseView from "./views/emoji-packs/browse";
-import EmojiPackView from "./views/emoji-packs/pack";
+import EmojiPackView from "./views/emoji-packs/emoji-pack";
import UserEmojiPacksTab from "./views/user/emoji-packs";
import EmojiPacksView from "./views/emoji-packs";
+import GoalsView from "./views/goals";
+import GoalsBrowseView from "./views/goals/browse";
+import GoalDetailsView from "./views/goals/goal-details";
+import UserGoalsTab from "./views/user/goals";
const StreamsView = React.lazy(() => import("./views/streams"));
const StreamView = React.lazy(() => import("./views/streams/stream"));
@@ -131,6 +135,7 @@ const router = createHashRouter([
{ path: "lists", element: },
{ path: "followers", element: },
{ path: "following", element: },
+ { path: "goals", element: },
{ path: "emojis", element: },
{ path: "relays", element: },
{ path: "reports", element: },
@@ -158,7 +163,15 @@ const router = createHashRouter([
children: [
{ path: "", element: },
{ path: "browse", element: },
- { path: ":addr", element: },
+ { path: ":addr", element: },
+ ],
+ },
+ {
+ path: "goals",
+ children: [
+ { path: "", element: },
+ { path: "browse", element: },
+ { path: ":id", element: },
],
},
{
diff --git a/src/components/embed-event/event-types/embedded-emoji-pack.tsx b/src/components/embed-event/event-types/embedded-emoji-pack.tsx
new file mode 100644
index 000000000..58d5f79ab
--- /dev/null
+++ b/src/components/embed-event/event-types/embedded-emoji-pack.tsx
@@ -0,0 +1,59 @@
+import {
+ ButtonGroup,
+ Card,
+ CardBody,
+ CardFooter,
+ CardHeader,
+ CardProps,
+ Flex,
+ Heading,
+ Image,
+ Link,
+ Text,
+} from "@chakra-ui/react";
+import { Link as RouterLink } from "react-router-dom";
+import dayjs from "dayjs";
+
+import { getSharableEventAddress } from "../../../helpers/nip19";
+import { getEmojisFromPack, getPackName } from "../../../helpers/nostr/emoji-packs";
+import { UserAvatarLink } from "../../user-avatar-link";
+import { UserLink } from "../../user-link";
+import EmojiPackFavoriteButton from "../../../views/emoji-packs/components/emoji-pack-favorite-button";
+import EmojiPackMenu from "../../../views/emoji-packs/components/emoji-pack-menu";
+import { NostrEvent } from "../../../types/nostr-event";
+
+export default function EmbeddedEmojiPack({ pack, ...props }: Omit & { pack: NostrEvent }) {
+ const emojis = getEmojisFromPack(pack);
+ const naddr = getSharableEventAddress(pack);
+
+ return (
+
+
+
+
+ {getPackName(pack)}
+
+
+ by
+
+
+
+
+
+
+
+
+ {emojis.length > 0 && (
+
+ {emojis.map(({ name, url }) => (
+
+ ))}
+
+ )}
+
+
+ Updated: {dayjs.unix(pack.created_at).fromNow()}
+
+
+ );
+}
diff --git a/src/components/embed-event/event-types/embedded-goal.tsx b/src/components/embed-event/event-types/embedded-goal.tsx
new file mode 100644
index 000000000..e3723465d
--- /dev/null
+++ b/src/components/embed-event/event-types/embedded-goal.tsx
@@ -0,0 +1,33 @@
+import { Card, CardBody, CardHeader, CardProps, Heading, Link, Text } from "@chakra-ui/react";
+import { Link as RouterLink } from "react-router-dom";
+
+import { getSharableEventAddress } from "../../../helpers/nip19";
+import { NostrEvent } from "../../../types/nostr-event";
+import { getGoalName } from "../../../helpers/nostr/goal";
+import { UserAvatarLink } from "../../user-avatar-link";
+import { UserLink } from "../../user-link";
+import GoalProgress from "../../../views/goals/components/goal-progress";
+import GoalZapButton from "../../../views/goals/components/goal-zap-button";
+
+export default function EmbeddedGoal({ goal, ...props }: Omit & { goal: NostrEvent }) {
+ const nevent = getSharableEventAddress(goal);
+
+ return (
+
+
+
+
+ {getGoalName(goal)}
+
+
+ by
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/note/embedded-note.tsx b/src/components/embed-event/event-types/embedded-note.tsx
similarity index 66%
rename from src/components/note/embedded-note.tsx
rename to src/components/embed-event/event-types/embedded-note.tsx
index ae794b314..7172f11ce 100644
--- a/src/components/note/embedded-note.tsx
+++ b/src/components/embed-event/event-types/embedded-note.tsx
@@ -1,17 +1,17 @@
import dayjs from "dayjs";
import { Button, Card, CardBody, CardHeader, Spacer, useDisclosure } from "@chakra-ui/react";
-import { NoteContents } from "./note-contents";
-import { NostrEvent } from "../../types/nostr-event";
-import { UserAvatarLink } from "../user-avatar-link";
-import { UserLink } from "../user-link";
-import { UserDnsIdentityIcon } from "../user-dns-identity-icon";
-import useSubject from "../../hooks/use-subject";
-import appSettings from "../../services/settings/app-settings";
-import EventVerificationIcon from "../event-verification-icon";
-import { TrustProvider } from "../../providers/trust";
-import { NoteLink } from "../note-link";
-import { ArrowDownSIcon, ArrowUpSIcon } from "../icons";
+import { NoteContents } from "../../note/note-contents";
+import { NostrEvent } from "../../../types/nostr-event";
+import { UserAvatarLink } from "../../user-avatar-link";
+import { UserLink } from "../../user-link";
+import { UserDnsIdentityIcon } from "../../user-dns-identity-icon";
+import useSubject from "../../../hooks/use-subject";
+import appSettings from "../../../services/settings/app-settings";
+import EventVerificationIcon from "../../event-verification-icon";
+import { TrustProvider } from "../../../providers/trust";
+import { NoteLink } from "../../note-link";
+import { ArrowDownSIcon, ArrowUpSIcon } from "../../icons";
export default function EmbeddedNote({ event }: { event: NostrEvent }) {
const { showSignatureVerification } = useSubject(appSettings);
diff --git a/src/components/embed-event/event-types/embedded-stream.tsx b/src/components/embed-event/event-types/embedded-stream.tsx
new file mode 100644
index 000000000..4c8ca9735
--- /dev/null
+++ b/src/components/embed-event/event-types/embedded-stream.tsx
@@ -0,0 +1,68 @@
+import { Card, CardBody, CardProps, Flex, Heading, Image, Link, Tag, Text, useBreakpointValue } from "@chakra-ui/react";
+import { Link as RouterLink, useNavigate } from "react-router-dom";
+import dayjs from "dayjs";
+
+import { parseStreamEvent } from "../../../helpers/nostr/stream";
+import { NostrEvent } from "../../../types/nostr-event";
+import StreamStatusBadge from "../../../views/streams/components/status-badge";
+import { UserLink } from "../../user-link";
+import { UserAvatar } from "../../user-avatar";
+import useEventNaddr from "../../../hooks/use-event-naddr";
+
+export default function EmbeddedStream({ event, ...props }: Omit & { event: NostrEvent }) {
+ const stream = parseStreamEvent(event);
+ const naddr = useEventNaddr(stream.event, stream.relays);
+ const isVertical = useBreakpointValue({ base: true, md: false });
+ const navigate = useNavigate();
+
+ return (
+
+
+
+ {isVertical ? (
+ navigate(`/streams/${naddr}`)}
+ maxH="2in"
+ mx="auto"
+ mb="2"
+ />
+ ) : (
+ navigate(`/streams/${naddr}`)}
+ />
+ )}
+
+
+
+ {stream.title}
+
+
+
+
+
+
+
+
+
+ {stream.starts && Started: {dayjs.unix(stream.starts).fromNow()}}
+ {stream.tags.length > 0 && (
+
+ {stream.tags.map((tag) => (
+ {tag}
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/embed-event/index.tsx b/src/components/embed-event/index.tsx
new file mode 100644
index 000000000..edadb33d2
--- /dev/null
+++ b/src/components/embed-event/index.tsx
@@ -0,0 +1,68 @@
+import type { DecodeResult } from "nostr-tools/lib/nip19";
+import { Link } from "@chakra-ui/react";
+
+import EmbeddedNote from "./event-types/embedded-note";
+import useSingleEvent from "../../hooks/use-single-event";
+import { NoteLink } from "../note-link";
+import { NostrEvent } from "../../types/nostr-event";
+import { Kind, nip19 } from "nostr-tools";
+import useReplaceableEvent from "../../hooks/use-replaceable-event";
+import RelayCard from "../../views/relays/components/relay-card";
+import { STREAM_KIND } from "../../helpers/nostr/stream";
+import { GOAL_KIND } from "../../helpers/nostr/goal";
+import GoalCard from "../../views/goals/components/goal-card";
+import { getSharableEventAddress, safeDecode } from "../../helpers/nip19";
+import EmbeddedStream from "./event-types/embedded-stream";
+import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs";
+import EmbeddedEmojiPack from "./event-types/embedded-emoji-pack";
+import { buildAppSelectUrl } from "../../helpers/nostr/apps";
+import EmbeddedGoal from "./event-types/embedded-goal";
+
+export function EmbedEvent({ event }: { event: NostrEvent }) {
+ switch (event.kind) {
+ case Kind.Text:
+ return ;
+ case STREAM_KIND:
+ return ;
+ case GOAL_KIND:
+ return ;
+ case EMOJI_PACK_KIND:
+ return ;
+ }
+
+ const address = getSharableEventAddress(event);
+ return (
+
+ {address}
+
+ );
+}
+
+export function EmbedEventPointer({ pointer }: { pointer: DecodeResult }) {
+ switch (pointer.type) {
+ case "note": {
+ const { event } = useSingleEvent(pointer.data);
+ if (event === undefined) return ;
+ return ;
+ }
+ case "nevent": {
+ const { event } = useSingleEvent(pointer.data.id, pointer.data.relays);
+ if (event === undefined) return ;
+ return ;
+ }
+ case "naddr": {
+ const event = useReplaceableEvent(pointer.data);
+ if (!event) return {nip19.naddrEncode(pointer.data)};
+ return ;
+ }
+ case "nrelay":
+ return ;
+ }
+ return null;
+}
+
+export function EmbedEventNostrLink({ link }: { link: string }) {
+ const pointer = safeDecode(link);
+
+ return pointer ? : <>{link}>;
+}
diff --git a/src/components/embed-types/common.tsx b/src/components/embed-types/common.tsx
index 8e9c22361..a6c943175 100644
--- a/src/components/embed-types/common.tsx
+++ b/src/components/embed-types/common.tsx
@@ -18,5 +18,5 @@ export function renderGenericUrl(match: URL) {
}
export function renderOpenGraphUrl(match: URL) {
- return ;
+ return ;
}
diff --git a/src/components/embed-types/nostr.tsx b/src/components/embed-types/nostr.tsx
index cc452568a..15bbe4e6c 100644
--- a/src/components/embed-types/nostr.tsx
+++ b/src/components/embed-types/nostr.tsx
@@ -1,4 +1,3 @@
-import { nip19 } from "nostr-tools";
import { EmbedableContent, embedJSX } from "../../helpers/embeds";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import QuoteNote from "../note/quote-note";
@@ -6,6 +5,8 @@ import { UserLink } from "../user-link";
import { Link } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { getMatchHashtag, getMatchNostrLink, stripInvisibleChar } from "../../helpers/regexp";
+import { safeDecode } from "../../helpers/nip19";
+import { EmbedEventPointer } from "../embed-event";
// nostr:nevent1qqsthg2qlxp9l7egtwa92t8lusm7pjknmjwa75ctrrpcjyulr9754fqpz3mhxue69uhhyetvv9ujuerpd46hxtnfduq36amnwvaz7tmwdaehgu3dwp6kytnhv4kxcmmjv3jhytnwv46q2qg5q9
// nostr:nevent1qqsq3wc73lqxd70lg43m5rul57d4mhcanttjat56e30yx5zla48qzlspz9mhxue69uhkummnw3e82efwvdhk6qgdwaehxw309ahx7uewd3hkcq5hsum
@@ -14,23 +15,21 @@ export function embedNostrLinks(content: EmbedableContent) {
name: "nostr-link",
regexp: getMatchNostrLink(),
render: (match) => {
- try {
- const decoded = nip19.decode(match[2]);
+ const decoded = safeDecode(match[2]);
+ if (!decoded) return null;
- switch (decoded.type) {
- case "npub":
- return ;
- case "nprofile":
- return ;
- case "note":
- return ;
- case "nevent":
- return ;
- default:
- return null;
- }
- } catch (e) {
- return null;
+ switch (decoded.type) {
+ case "npub":
+ return ;
+ case "nprofile":
+ return ;
+ case "note":
+ case "nevent":
+ case "naddr":
+ case "nrelay":
+ return ;
+ default:
+ return null;
}
},
});
diff --git a/src/components/icons.tsx b/src/components/icons.tsx
index 52a6fc2fd..2e063cc00 100644
--- a/src/components/icons.tsx
+++ b/src/components/icons.tsx
@@ -373,3 +373,9 @@ export const EmojiIcon = createIcon({
d: "M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM12 20C16.4183 20 20 16.4183 20 12C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20ZM7 12H9C9 13.6569 10.3431 15 12 15C13.6569 15 15 13.6569 15 12H17C17 14.7614 14.7614 17 12 17C9.23858 17 7 14.7614 7 12Z",
defaultProps,
});
+
+export const GoalIcon = createIcon({
+ displayName: "GoalIcon",
+ d: "M5 3V19H21V21H3V3H5ZM20.2929 6.29289L21.7071 7.70711L16 13.4142L13 10.415L8.70711 14.7071L7.29289 13.2929L13 7.58579L16 10.585L20.2929 6.29289Z",
+ defaultProps,
+});
diff --git a/src/components/layout/nav-items.tsx b/src/components/layout/nav-items.tsx
index 8557c1bcf..ac304ab12 100644
--- a/src/components/layout/nav-items.tsx
+++ b/src/components/layout/nav-items.tsx
@@ -4,6 +4,7 @@ import {
ChatIcon,
EmojiIcon,
FeedIcon,
+ GoalIcon,
ListIcon,
LiveStreamIcon,
MapIcon,
@@ -49,6 +50,9 @@ export default function NavItems({ isInDrawer = false }: { isInDrawer?: boolean
+
diff --git a/src/components/note-link.tsx b/src/components/note-link.tsx
index 1e54e4992..3f7b0858b 100644
--- a/src/components/note-link.tsx
+++ b/src/components/note-link.tsx
@@ -1,7 +1,6 @@
import { useMemo } from "react";
import { Link, LinkProps } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
-import { truncatedId } from "../helpers/nostr/events";
import { nip19 } from "nostr-tools";
import { getSharableNoteId } from "../helpers/nip19";
@@ -14,7 +13,7 @@ export const NoteLink = ({ children, noteId, color = "blue.500", ...props }: Not
return (
- {children || truncatedId(nip19.noteEncode(noteId))}
+ {children || nip19.noteEncode(noteId)}
);
};
diff --git a/src/components/note/note-menu.tsx b/src/components/note/note-menu.tsx
index 179829bfd..b090cdbdb 100644
--- a/src/components/note/note-menu.tsx
+++ b/src/components/note/note-menu.tsx
@@ -3,7 +3,7 @@ import { MenuItem, useDisclosure } from "@chakra-ui/react";
import { useCopyToClipboard } from "react-use";
import { nip19 } from "nostr-tools";
-import { getSharableNoteId } from "../../helpers/nip19";
+import { getSharableEventAddress } from "../../helpers/nip19";
import { NostrEvent } from "../../types/nostr-event";
import { MenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
@@ -39,19 +39,20 @@ export const NoteMenu = ({ event, ...props }: { event: NostrEvent } & Omit
}>
Zaps/Reactions
-
-
+ )} */}
+ }>
+ View Raw
+
+
+
+ {infoModal.isOpen && (
+
+ )}
+ >
+ );
+}
diff --git a/src/views/goals/components/goal-progress.tsx b/src/views/goals/components/goal-progress.tsx
new file mode 100644
index 000000000..ba770d982
--- /dev/null
+++ b/src/views/goals/components/goal-progress.tsx
@@ -0,0 +1,24 @@
+import { Flex, Progress, Text } from "@chakra-ui/react";
+import { NostrEvent } from "../../../types/nostr-event";
+import { getGoalAmount, getGoalRelays } from "../../../helpers/nostr/goal";
+import { LightningIcon } from "../../../components/icons";
+import useEventZaps from "../../../hooks/use-event-zaps";
+import { getEventUID } from "../../../helpers/nostr/events";
+import { totalZaps } from "../../../helpers/zaps";
+import { readablizeSats } from "../../../helpers/bolt11";
+
+export default function GoalProgress({ goal }: { goal: NostrEvent }) {
+ const amount = getGoalAmount(goal);
+ const zaps = useEventZaps(getEventUID(goal), getGoalRelays(goal), true);
+ const raised = totalZaps(zaps);
+
+ return (
+
+
+
+
+ {readablizeSats(raised / 1000)} / {readablizeSats(amount / 1000)} ({Math.round((raised / amount) * 1000) / 10}%)
+
+
+ );
+}
diff --git a/src/views/goals/components/goal-zap-button.tsx b/src/views/goals/components/goal-zap-button.tsx
new file mode 100644
index 000000000..8dccefbf6
--- /dev/null
+++ b/src/views/goals/components/goal-zap-button.tsx
@@ -0,0 +1,45 @@
+import { Button, ButtonProps, useDisclosure } from "@chakra-ui/react";
+import { NostrEvent } from "../../../types/nostr-event";
+import ZapModal from "../../../components/zap-modal";
+import eventZapsService from "../../../services/event-zaps";
+import { getEventUID } from "../../../helpers/nostr/events";
+import { useInvoiceModalContext } from "../../../providers/invoice-modal";
+import { getGoalRelays } from "../../../helpers/nostr/goal";
+import { useReadRelayUrls } from "../../../hooks/use-client-relays";
+
+export default function GoalZapButton({
+ goal,
+ ...props
+}: Omit & { goal: NostrEvent }) {
+ const modal = useDisclosure();
+ const { requestPay } = useInvoiceModalContext();
+
+ const readRelays = useReadRelayUrls(getGoalRelays(goal));
+ const handleInvoice = async (invoice: string) => {
+ modal.onClose();
+ await requestPay(invoice);
+ setTimeout(() => {
+ eventZapsService.requestZaps(getEventUID(goal), readRelays, true);
+ }, 1000);
+ };
+
+ return (
+ <>
+
+ {modal.isOpen && (
+
+ )}
+ >
+ );
+}
diff --git a/src/views/goals/components/goal-zap-list.tsx b/src/views/goals/components/goal-zap-list.tsx
new file mode 100644
index 000000000..d185a590a
--- /dev/null
+++ b/src/views/goals/components/goal-zap-list.tsx
@@ -0,0 +1,39 @@
+import { Box, Flex, Spacer, Text } from "@chakra-ui/react";
+import { getEventUID } from "../../../helpers/nostr/events";
+import { getGoalRelays } from "../../../helpers/nostr/goal";
+import useEventZaps from "../../../hooks/use-event-zaps";
+import { NostrEvent } from "../../../types/nostr-event";
+import { UserAvatarLink } from "../../../components/user-avatar-link";
+import { UserLink } from "../../../components/user-link";
+import { readablizeSats } from "../../../helpers/bolt11";
+import { LightningIcon } from "../../../components/icons";
+import dayjs from "dayjs";
+
+export default function GoalZapList({ goal }: { goal: NostrEvent }) {
+ const zaps = useEventZaps(getEventUID(goal), getGoalRelays(goal), true);
+
+ const sorted = Array.from(zaps).sort((a, b) => b.event.created_at - a.event.created_at);
+
+ return (
+ <>
+ {sorted.map((zap) => (
+
+
+
+
+
+ {dayjs.unix(zap.event.created_at).fromNow()}
+
+ {zap.request.content && {zap.request.content}}
+
+
+ {zap.payment.amount && (
+
+ {readablizeSats(zap.payment.amount / 1000)}
+
+ )}
+
+ ))}
+ >
+ );
+}
diff --git a/src/views/goals/goal-details.tsx b/src/views/goals/goal-details.tsx
new file mode 100644
index 000000000..6134d5985
--- /dev/null
+++ b/src/views/goals/goal-details.tsx
@@ -0,0 +1,71 @@
+import { useNavigate, useParams } from "react-router-dom";
+import { nip19 } from "nostr-tools";
+
+import { Button, ButtonGroup, Divider, Flex, Heading, Spacer, Spinner } from "@chakra-ui/react";
+import { ArrowLeftSIcon } from "../../components/icons";
+import GoalMenu from "./components/goal-menu";
+import { getGoalAmount, getGoalName } from "../../helpers/nostr/goal";
+import GoalProgress from "./components/goal-progress";
+import useSingleEvent from "../../hooks/use-single-event";
+import { isHexKey } from "../../helpers/nip19";
+import { EventPointer } from "nostr-tools/lib/nip19";
+import { UserAvatar } from "../../components/user-avatar";
+import { UserLink } from "../../components/user-link";
+import GoalContents from "./components/goal-contents";
+import GoalZapList from "./components/goal-zap-list";
+import { readablizeSats } from "../../helpers/bolt11";
+import GoalZapButton from "./components/goal-zap-button";
+
+function useGoalPointerFromParams(): EventPointer {
+ const { id } = useParams() as { id: string };
+ if (isHexKey(id)) return { id };
+ const parsed = nip19.decode(id);
+ if (parsed.type === "nevent") return parsed.data;
+ if (parsed.type === "note") return { id: parsed.data };
+ throw new Error("bad goal id");
+}
+
+export default function GoalDetailsView() {
+ const navigate = useNavigate();
+ const pointer = useGoalPointerFromParams();
+
+ const { event: goal } = useSingleEvent(pointer.id, pointer.relays);
+
+ if (!goal) return ;
+
+ return (
+
+
+
+
+
+ {getGoalName(goal)} ({readablizeSats(getGoalAmount(goal) / 1000)})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Progress:
+
+
+
+ Contributors:
+
+
+
+
+ );
+}
diff --git a/src/views/goals/index.tsx b/src/views/goals/index.tsx
new file mode 100644
index 000000000..972fa56f8
--- /dev/null
+++ b/src/views/goals/index.tsx
@@ -0,0 +1,81 @@
+import { Button, Center, Divider, Flex, Heading, Link, SimpleGrid, Spacer } from "@chakra-ui/react";
+import { Navigate, Link as RouterLink } from "react-router-dom";
+
+import { useCurrentAccount } from "../../hooks/use-current-account";
+import { ExternalLinkIcon } from "../../components/icons";
+import { getEventUID } from "../../helpers/nostr/events";
+import { useReadRelayUrls } from "../../hooks/use-client-relays";
+import useTimelineLoader from "../../hooks/use-timeline-loader";
+import useSubject from "../../hooks/use-subject";
+import GoalCard from "./components/goal-card";
+import { GOAL_KIND } from "../../helpers/nostr/goal";
+
+function UserGoalsManagerPage() {
+ const account = useCurrentAccount()!;
+
+ const readRelays = useReadRelayUrls();
+ const timeline = useTimelineLoader(
+ `${account.pubkey}-goals`,
+ readRelays,
+ {
+ authors: [account.pubkey],
+ kinds: [GOAL_KIND],
+ },
+ { enabled: !!account.pubkey },
+ );
+
+ const goals = useSubject(timeline.timeline);
+
+ if (goals.length === 0) {
+ return (
+
+ You don't have any goals,{" "}
+
+ Find a goal
+ {" "}
+ to support or{" "}
+
+ Create one
+
+
+ );
+ }
+
+ return (
+ <>
+ {goals.length > 0 && (
+ <>
+
+ Created goals
+
+
+
+ {goals.map((event) => (
+
+ ))}
+
+ >
+ )}
+ >
+ );
+}
+
+export default function GoalsView() {
+ const account = useCurrentAccount();
+
+ return (
+
+
+
+
+ }>
+ Goal manager
+
+
+
+ {account ? : }
+
+ );
+}
diff --git a/src/views/lists/components/list-card.tsx b/src/views/lists/components/list-card.tsx
index aef89c9f6..9e82e82dd 100644
--- a/src/views/lists/components/list-card.tsx
+++ b/src/views/lists/components/list-card.tsx
@@ -6,6 +6,7 @@ import {
CardBody,
CardFooter,
CardHeader,
+ CardProps,
Flex,
Heading,
Link,
@@ -17,7 +18,7 @@ import dayjs from "dayjs";
import { UserAvatarLink } from "../../../components/user-avatar-link";
import { UserLink } from "../../../components/user-link";
import { getEventsFromList, getListName, getPubkeysFromList } from "../../../helpers/nostr/lists";
-import { getSharableEventNaddr } from "../../../helpers/nip19";
+import { getSharableEventAddress } from "../../../helpers/nip19";
import { NostrEvent } from "../../../types/nostr-event";
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
import { createCoordinate } from "../../../services/replaceable-event-requester";
@@ -29,18 +30,18 @@ import ListFavoriteButton from "./list-favorite-button";
import { getEventUID } from "../../../helpers/nostr/events";
import ListMenu from "./list-menu";
-function ListCardRender({ event }: { event: NostrEvent }) {
+function ListCardRender({ event, ...props }: Omit & { event: NostrEvent }) {
const people = getPubkeysFromList(event);
const notes = getEventsFromList(event);
const link =
- event.kind === Kind.Contacts ? createCoordinate(Kind.Contacts, event.pubkey) : getSharableEventNaddr(event);
+ event.kind === Kind.Contacts ? createCoordinate(Kind.Contacts, event.pubkey) : getSharableEventAddress(event);
// if there is a parent intersection observer, register this card
const ref = useRef(null);
useRegisterIntersectionEntity(ref, getEventUID(event));
return (
-
+
diff --git a/src/views/lists/components/list-menu.tsx b/src/views/lists/components/list-menu.tsx
index b9d9a25e6..b09715db2 100644
--- a/src/views/lists/components/list-menu.tsx
+++ b/src/views/lists/components/list-menu.tsx
@@ -6,7 +6,7 @@ import { MenuIconButton, MenuIconButtonProps } from "../../../components/menu-ic
import { useCurrentAccount } from "../../../hooks/use-current-account";
import NoteDebugModal from "../../../components/debug-modals/note-debug-modal";
import { CodeIcon, ExternalLinkIcon, RepostIcon, TrashIcon } from "../../../components/icons";
-import { getSharableEventNaddr } from "../../../helpers/nip19";
+import { getSharableEventAddress } from "../../../helpers/nip19";
import { buildAppSelectUrl } from "../../../helpers/nostr/apps";
import { useDeleteEventContext } from "../../../providers/delete-event-provider";
@@ -18,7 +18,7 @@ export default function ListMenu({ list, ...props }: { list: NostrEvent } & Omit
const [_clipboardState, copyToClipboard] = useCopyToClipboard();
- const naddr = getSharableEventNaddr(list);
+ const naddr = getSharableEventAddress(list);
return (
<>
diff --git a/src/views/lists/index.tsx b/src/views/lists/index.tsx
index a5584bfe9..9b24810c1 100644
--- a/src/views/lists/index.tsx
+++ b/src/views/lists/index.tsx
@@ -9,7 +9,7 @@ import ListCard from "./components/list-card";
import { getEventUID } from "../../helpers/nostr/events";
import useUserLists from "../../hooks/use-user-lists";
import NewListModal from "./components/new-list-modal";
-import { getSharableEventNaddr } from "../../helpers/nip19";
+import { getSharableEventAddress } from "../../helpers/nip19";
import { MUTE_LIST_KIND, NOTE_LIST_KIND, PEOPLE_LIST_KIND, PIN_LIST_KIND } from "../../helpers/nostr/lists";
import useFavoriteLists from "../../hooks/use-favorite-lists";
@@ -89,7 +89,7 @@ function ListsPage() {
navigate(`/lists/${getSharableEventNaddr(list)}`)}
+ onCreated={(list) => navigate(`/lists/${getSharableEventAddress(list)}`)}
/>
)}
diff --git a/src/views/lists/list.tsx b/src/views/lists/list-details.tsx
similarity index 98%
rename from src/views/lists/list.tsx
rename to src/views/lists/list-details.tsx
index e7af031bc..e8fc30580 100644
--- a/src/views/lists/list.tsx
+++ b/src/views/lists/list-details.tsx
@@ -31,7 +31,7 @@ function useListCoordinate() {
return parsed.data;
}
-export default function ListView() {
+export default function ListDetailsView() {
const navigate = useNavigate();
const coordinate = useListCoordinate();
const { deleteEvent } = useDeleteEventContext();
diff --git a/src/views/relays/components/relay-card.tsx b/src/views/relays/components/relay-card.tsx
index d96b6bb0b..9f2f85462 100644
--- a/src/views/relays/components/relay-card.tsx
+++ b/src/views/relays/components/relay-card.tsx
@@ -1,3 +1,4 @@
+import { PropsWithChildren } from "react";
import {
Box,
Button,
@@ -21,9 +22,10 @@ import {
ModalOverlay,
Tag,
useDisclosure,
- useToast,
} from "@chakra-ui/react";
+import styled from "@emotion/styled";
import { Link as RouterLink } from "react-router-dom";
+
import { useRelayInfo } from "../../../hooks/use-relay-info";
import { RelayFavicon } from "../../../components/relay-favicon";
import { CodeIcon, RepostIcon } from "../../../components/icons";
@@ -34,13 +36,8 @@ import clientRelaysService from "../../../services/client-relays";
import { RelayMode } from "../../../classes/relay";
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
import { useCurrentAccount } from "../../../hooks/use-current-account";
-import styled from "@emotion/styled";
-import { PropsWithChildren, useCallback } from "react";
import RawJson from "../../../components/debug-modals/raw-json";
-import { DraftNostrEvent } from "../../../types/nostr-event";
-import dayjs from "dayjs";
-import { useSigningContext } from "../../../providers/signing-provider";
-import NostrPublishAction from "../../../classes/nostr-publish-action";
+import { RelayShareButton } from "./relay-share-button";
const B = styled.span`
font-weight: bold;
@@ -146,44 +143,6 @@ export function RelayDebugButton({ url, ...props }: { url: string } & Omit) {
- const toast = useToast();
- const { requestSignature } = useSigningContext();
-
- const recommendRelay = useCallback(async () => {
- try {
- const writeRelays = clientRelaysService.getWriteUrls();
-
- const draft: DraftNostrEvent = {
- kind: 2,
- content: relay,
- tags: [],
- created_at: dayjs().unix(),
- };
-
- const signed = await requestSignature(draft);
- const post = new NostrPublishAction("Share Relay", writeRelays, signed);
- await post.onComplete;
- } catch (e) {
- if (e instanceof Error) toast({ description: e.message, status: "error" });
- }
- }, []);
-
- return (
- }
- aria-label="Recommend Relay"
- title="Recommend Relay"
- onClick={recommendRelay}
- variant="ghost"
- {...props}
- />
- );
-}
-
export function RelayPaidTag({ url }: { url: string }) {
const { info } = useRelayInfo(url);
diff --git a/src/views/relays/components/relay-share-button.tsx b/src/views/relays/components/relay-share-button.tsx
new file mode 100644
index 000000000..ebd2945b8
--- /dev/null
+++ b/src/views/relays/components/relay-share-button.tsx
@@ -0,0 +1,47 @@
+import { useCallback } from "react";
+import dayjs from "dayjs";
+import { IconButton, IconButtonProps, useToast } from "@chakra-ui/react";
+
+import { useSigningContext } from "../../../providers/signing-provider";
+import clientRelaysService from "../../../services/client-relays";
+import { DraftNostrEvent } from "../../../types/nostr-event";
+import NostrPublishAction from "../../../classes/nostr-publish-action";
+import { RepostIcon } from "../../../components/icons";
+
+export function RelayShareButton({
+ relay,
+ ...props
+}: { relay: string } & Omit) {
+ const toast = useToast();
+ const { requestSignature } = useSigningContext();
+
+ const recommendRelay = useCallback(async () => {
+ try {
+ const writeRelays = clientRelaysService.getWriteUrls();
+
+ const draft: DraftNostrEvent = {
+ kind: 2,
+ content: relay,
+ tags: [],
+ created_at: dayjs().unix(),
+ };
+
+ const signed = await requestSignature(draft);
+ const post = new NostrPublishAction("Share Relay", writeRelays, signed);
+ await post.onComplete;
+ } catch (e) {
+ if (e instanceof Error) toast({ description: e.message, status: "error" });
+ }
+ }, []);
+
+ return (
+ }
+ aria-label="Recommend Relay"
+ title="Recommend Relay"
+ onClick={recommendRelay}
+ variant="ghost"
+ {...props}
+ />
+ );
+}
diff --git a/src/views/streams/components/status-badge.tsx b/src/views/streams/components/status-badge.tsx
index 2253f03dd..50d13da41 100644
--- a/src/views/streams/components/status-badge.tsx
+++ b/src/views/streams/components/status-badge.tsx
@@ -8,13 +8,13 @@ export default function StreamStatusBadge({
switch (stream.status) {
case "live":
return (
-
+
live
);
case "ended":
return (
-
+
ended
);
diff --git a/src/views/streams/components/streamer-cards.tsx b/src/views/streams/components/streamer-cards.tsx
index aee0a67d3..be740aaec 100644
--- a/src/views/streams/components/streamer-cards.tsx
+++ b/src/views/streams/components/streamer-cards.tsx
@@ -34,7 +34,7 @@ function StreamerCard({ cord, relay, ...props }: { cord: string; relay?: string
const link = card.tags.find((t) => t[0] === "r")?.[1];
return (
-
+
{image && }
{title && (
@@ -42,10 +42,10 @@ function StreamerCard({ cord, relay, ...props }: { cord: string; relay?: string
)}
-
+
{link && (
- {link}
+ {!image && link}
)}
diff --git a/src/views/streams/stream/stream-chat/chat-message-form.tsx b/src/views/streams/stream/stream-chat/chat-message-form.tsx
index 9e5542944..dc8463bf4 100644
--- a/src/views/streams/stream/stream-chat/chat-message-form.tsx
+++ b/src/views/streams/stream/stream-chat/chat-message-form.tsx
@@ -78,7 +78,7 @@ export default function ChatMessageForm({ stream }: { stream: ParsedStream }) {
{zapModal.isOpen && (
{
reset();
diff --git a/src/views/user/goals.tsx b/src/views/user/goals.tsx
new file mode 100644
index 000000000..d33fc143f
--- /dev/null
+++ b/src/views/user/goals.tsx
@@ -0,0 +1,36 @@
+import { useOutletContext } from "react-router-dom";
+import { Divider, Flex, Heading, SimpleGrid } from "@chakra-ui/react";
+
+import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
+import useTimelineLoader from "../../hooks/use-timeline-loader";
+import useSubject from "../../hooks/use-subject";
+import { getEventUID } from "../../helpers/nostr/events";
+import IntersectionObserverProvider from "../../providers/intersection-observer";
+import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
+import { GOAL_KIND } from "../../helpers/nostr/goal";
+import GoalCard from "../goals/components/goal-card";
+
+export default function UserGoalsTab() {
+ const { pubkey } = useOutletContext() as { pubkey: string };
+ const readRelays = useAdditionalRelayContext();
+
+ const timeline = useTimelineLoader(pubkey + "-goals", readRelays, {
+ authors: [pubkey],
+ kinds: [GOAL_KIND],
+ });
+ const goals = useSubject(timeline.timeline);
+
+ const callback = useTimelineCurserIntersectionCallback(timeline);
+
+ return (
+
+
+
+ {goals.map((goal) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/views/user/index.tsx b/src/views/user/index.tsx
index 329dff4b9..45112453f 100644
--- a/src/views/user/index.tsx
+++ b/src/views/user/index.tsx
@@ -50,6 +50,7 @@ const tabs = [
{ label: "Following", path: "following" },
{ label: "Likes", path: "likes" },
{ label: "Relays", path: "relays" },
+ { label: "Goals", path: "goals" },
{ label: "Emoji Packs", path: "emojis" },
{ label: "Reports", path: "reports" },
{ label: "Followers", path: "followers" },
diff --git a/src/views/user/relays.tsx b/src/views/user/relays.tsx
index dd7db5259..23ce8751d 100644
--- a/src/views/user/relays.tsx
+++ b/src/views/user/relays.tsx
@@ -9,11 +9,12 @@ import useSubject from "../../hooks/use-subject";
import { NostrEvent } from "../../types/nostr-event";
import RelayReviewNote from "../relays/components/relay-review-note";
import { RelayFavicon } from "../../components/relay-favicon";
-import { RelayDebugButton, RelayJoinAction, RelayMetadata, RelayShareButton } from "../relays/components/relay-card";
+import { RelayDebugButton, RelayJoinAction, RelayMetadata } from "../relays/components/relay-card";
import IntersectionObserverProvider from "../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import { useRelayInfo } from "../../hooks/use-relay-info";
import { ErrorBoundary } from "../../components/error-boundary";
+import { RelayShareButton } from "../relays/components/relay-share-button";
function Relay({ url, reviews }: { url: string; reviews: NostrEvent[] }) {
const { info } = useRelayInfo(url);