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 (