diff --git a/.changeset/proud-forks-fry.md b/.changeset/proud-forks-fry.md
new file mode 100644
index 000000000..a25b7999b
--- /dev/null
+++ b/.changeset/proud-forks-fry.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Add option to pin notes
diff --git a/.changeset/spicy-flowers-march.md b/.changeset/spicy-flowers-march.md
new file mode 100644
index 000000000..39c1baca9
--- /dev/null
+++ b/.changeset/spicy-flowers-march.md
@@ -0,0 +1,5 @@
+---
+"nostrudel": minor
+---
+
+Show pinned notes on user profile
diff --git a/src/components/icons.tsx b/src/components/icons.tsx
index 0706300a8..f088b9a20 100644
--- a/src/components/icons.tsx
+++ b/src/components/icons.tsx
@@ -60,6 +60,7 @@ import Wallet02 from "./icons/wallet-02";
import Download01 from "./icons/download-01";
import Repeat01 from "./icons/repeat-01";
import ReverseLeft from "./icons/reverse-left";
+import Pin01 from "./icons/pin-01";
const defaultProps: IconProps = { boxSize: 4 };
@@ -89,6 +90,7 @@ export const ChevronRightIcon = ChevronRight;
export const LightningIcon = Zap;
export const RelayIcon = Server04;
export const BroadcastEventIcon = Share07;
+export const PinIcon = Pin01;
export const ExternalLinkIcon = Share04;
diff --git a/src/components/note/components/repost-modal.tsx b/src/components/note/components/repost-modal.tsx
index 8c1ecfa21..f4712464e 100644
--- a/src/components/note/components/repost-modal.tsx
+++ b/src/components/note/components/repost-modal.tsx
@@ -24,7 +24,7 @@ import NostrPublishAction from "../../../classes/nostr-publish-action";
import clientRelaysService from "../../../services/client-relays";
import { useSigningContext } from "../../../providers/signing-provider";
import { ChevronDownIcon, ChevronUpIcon, ExternalLinkIcon } from "../../icons";
-import useJoinedCommunitiesList from "../../../hooks/use-communities-joined-list";
+import useUserCommunitiesList from "../../../hooks/use-user-communities-list";
import useCurrentAccount from "../../../hooks/use-current-account";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import { createCoordinate } from "../../../services/replaceable-event-requester";
@@ -54,7 +54,7 @@ export default function RepostModal({
const toast = useToast();
const { requestSignature } = useSigningContext();
const showCommunities = useDisclosure();
- const { pointers } = useJoinedCommunitiesList(account?.pubkey);
+ const { pointers } = useUserCommunitiesList(account?.pubkey);
const [loading, setLoading] = useState(false);
const repost = async (communityPointer?: AddressPointer) => {
diff --git a/src/components/note/note-menu.tsx b/src/components/note/note-menu.tsx
index 0fea76815..3b336c8ce 100644
--- a/src/components/note/note-menu.tsx
+++ b/src/components/note/note-menu.tsx
@@ -1,11 +1,8 @@
-import { useCallback } from "react";
-import { MenuItem, useDisclosure } from "@chakra-ui/react";
+import { useCallback, useState } from "react";
+import { MenuItem, useDisclosure, useToast } from "@chakra-ui/react";
import { useCopyToClipboard } from "react-use";
import { nip19 } from "nostr-tools";
-
-import { getSharableEventAddress } from "../../helpers/nip19";
-import { NostrEvent } from "../../types/nostr-event";
-import { CustomMenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
+import dayjs from "dayjs";
import {
BroadcastEventIcon,
@@ -17,7 +14,11 @@ import {
RepostIcon,
TrashIcon,
UnmuteIcon,
+ PinIcon,
} from "../icons";
+import { getSharableEventAddress } from "../../helpers/nip19";
+import { DraftNostrEvent, NostrEvent, isETag } from "../../types/nostr-event";
+import { CustomMenuIconButton, MenuIconButtonProps } from "../menu-icon-button";
import NoteReactionsModal from "./note-zaps-modal";
import NoteDebugModal from "../debug-modals/note-debug-modal";
import useCurrentAccount from "../../hooks/use-current-account";
@@ -30,6 +31,49 @@ import useUserMuteFunctions from "../../hooks/use-user-mute-functions";
import { useMuteModalContext } from "../../providers/mute-modal-provider";
import NoteTranslationModal from "../note-translation-modal";
import Translate01 from "../icons/translate-01";
+import useUserPinList from "../../hooks/use-user-pin-list";
+import { useSigningContext } from "../../providers/signing-provider";
+import { PIN_LIST_KIND, listAddEvent, listRemoveEvent } from "../../helpers/nostr/lists";
+
+function PinNoteItem({ event }: { event: NostrEvent }) {
+ const toast = useToast();
+ const account = useCurrentAccount();
+ const { requestSignature } = useSigningContext();
+ const { list } = useUserPinList(account?.pubkey);
+
+ const isPinned = list?.tags.some((t) => isETag(t) && t[1] === event.id) ?? false;
+ const label = isPinned ? "Unpin Note" : "Pin Note";
+
+ const [loading, setLoading] = useState(false);
+ const togglePin = useCallback(async () => {
+ try {
+ setLoading(true);
+ let draft: DraftNostrEvent = {
+ kind: PIN_LIST_KIND,
+ created_at: dayjs().unix(),
+ content: list?.content ?? "",
+ tags: list?.tags ? Array.from(list.tags) : [],
+ };
+
+ if (isPinned) draft = listRemoveEvent(draft, event.id);
+ else draft = listAddEvent(draft, event.id);
+
+ const signed = await requestSignature(draft);
+ new NostrPublishAction(label, clientRelaysService.getWriteUrls(), signed);
+ setLoading(false);
+ } catch (e) {
+ if (e instanceof Error) toast({ status: "error", description: e.message });
+ }
+ }, [list, isPinned]);
+
+ if (event.pubkey !== account?.pubkey) return null;
+
+ return (
+ } isDisabled={loading || account.readonly}>
+ {label}
+
+ );
+}
export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Omit) {
const account = useCurrentAccount();
@@ -90,6 +134,7 @@ export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Om
}>
Broadcast
+
}>
View Raw
diff --git a/src/components/post-modal/community-select.tsx b/src/components/post-modal/community-select.tsx
index c9a2cb0f0..442f822b7 100644
--- a/src/components/post-modal/community-select.tsx
+++ b/src/components/post-modal/community-select.tsx
@@ -1,7 +1,7 @@
import { forwardRef } from "react";
import { Select, SelectProps } from "@chakra-ui/react";
-import useJoinedCommunitiesList from "../../hooks/use-communities-joined-list";
+import useUserCommunitiesList from "../../hooks/use-user-communities-list";
import useCurrentAccount from "../../hooks/use-current-account";
import { getCommunityName } from "../../helpers/nostr/communities";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
@@ -17,7 +17,7 @@ function CommunityOption({ pointer }: { pointer: AddressPointer }) {
const CommunitySelect = forwardRef>(({ ...props }, ref) => {
const account = useCurrentAccount();
- const { pointers } = useJoinedCommunitiesList(account?.pubkey);
+ const { pointers } = useUserCommunitiesList(account?.pubkey);
return (