mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-26 17:52:18 +01:00
Merge branch 'next'
This commit is contained in:
commit
0b08d61f42
5
.changeset/proud-forks-fry.md
Normal file
5
.changeset/proud-forks-fry.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Add option to pin notes
|
5
.changeset/spicy-flowers-march.md
Normal file
5
.changeset/spicy-flowers-march.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Show pinned notes on user profile
|
@ -10,7 +10,14 @@ export function embedEmoji(content: EmbedableContent, note: NostrEvent | DraftNo
|
||||
const emojiTag = note.tags.filter(isEmojiTag).find((t) => t[1].toLowerCase() === match[1].toLowerCase());
|
||||
if (emojiTag) {
|
||||
return (
|
||||
<Image src={emojiTag[2]} h="1.2em" w="1.2em" display="inline-block" verticalAlign="middle" title={match[1]} />
|
||||
<Image
|
||||
src={emojiTag[2]}
|
||||
h="1.5em"
|
||||
maxW="3em"
|
||||
display="inline-block"
|
||||
verticalAlign="middle"
|
||||
title={match[1]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -1,5 +1,11 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { MouseEventHandler, useCallback, useState } from "react";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
@ -14,6 +20,7 @@ import {
|
||||
ModalOverlay,
|
||||
ModalProps,
|
||||
Select,
|
||||
Spacer,
|
||||
Spinner,
|
||||
Text,
|
||||
useToast,
|
||||
@ -43,65 +50,53 @@ function getTranslationRequestLanguage(request: NostrEvent) {
|
||||
return codes.find((code) => code.iso639_1 === targetLanguage);
|
||||
}
|
||||
|
||||
function TranslationResult({ result, request }: { result: NostrEvent; request?: NostrEvent }) {
|
||||
const requester = result.tags.find(isPTag)?.[1];
|
||||
const lang = request && getTranslationRequestLanguage(request);
|
||||
|
||||
return (
|
||||
<Card variant="outline">
|
||||
<CardHeader px="4" py="4" pb="2" display="flex" gap="2" alignItems="center">
|
||||
<UserAvatarLink pubkey={result.pubkey} size="sm" />
|
||||
<UserLink pubkey={result.pubkey} fontWeight="bold" />
|
||||
{lang && <Text>Translated to {lang.nativeName}</Text>}
|
||||
<Timestamp timestamp={result.created_at} />
|
||||
</CardHeader>
|
||||
<CardBody px="4" pt="0" pb="4">
|
||||
{requester && (
|
||||
<Text fontStyle="italic" mb="2">
|
||||
Requested by <UserLink pubkey={requester} fontWeight="bold" />
|
||||
</Text>
|
||||
)}
|
||||
<NoteContents event={result} />
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function TranslationRequest({ request }: { request: NostrEvent }) {
|
||||
const lang = getTranslationRequestLanguage(request);
|
||||
const requestRelays = request.tags.find((t) => t[0] === "relays")?.slice(1);
|
||||
const readRelays = useReadRelayUrls();
|
||||
|
||||
const timeline = useTimelineLoader(`${getEventUID(request)}-offers`, requestRelays || readRelays, {
|
||||
kinds: [DMV_STATUS_KIND],
|
||||
const timeline = useTimelineLoader(`${getEventUID(request)}-offers-results`, requestRelays || readRelays, {
|
||||
kinds: [DMV_STATUS_KIND, DMV_TRANSLATE_RESULT_KIND],
|
||||
"#e": [request.id],
|
||||
});
|
||||
|
||||
const offers = useSubject(timeline.timeline);
|
||||
const events = useSubject(timeline.timeline);
|
||||
const dvmStatuses: Record<string, NostrEvent> = {};
|
||||
for (const event of events) {
|
||||
if (
|
||||
(event.kind === DMV_STATUS_KIND || event.kind === DMV_TRANSLATE_RESULT_KIND) &&
|
||||
(!dvmStatuses[event.pubkey] || dvmStatuses[event.pubkey].created_at < event.created_at)
|
||||
) {
|
||||
dvmStatuses[event.pubkey] = event;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card variant="outline">
|
||||
<CardHeader px="4" py="4" pb="2" display="flex" gap="2" alignItems="center" flexWrap="wrap">
|
||||
<UserAvatarLink pubkey={request.pubkey} size="sm" />
|
||||
<UserLink pubkey={request.pubkey} fontWeight="bold" />
|
||||
<Text>Requested translation to {lang?.nativeName}</Text>
|
||||
<Text>
|
||||
Requested translation to <strong>{lang?.nativeName}</strong>
|
||||
</Text>
|
||||
<Timestamp timestamp={request.created_at} />
|
||||
</CardHeader>
|
||||
<CardBody px="4" pt="0" pb="4">
|
||||
{offers.length === 0 ? (
|
||||
<Flex gap="2" alignItems="center">
|
||||
<Spinner />
|
||||
Waiting for offers
|
||||
</Flex>
|
||||
) : (
|
||||
<Heading size="md" mb="2">
|
||||
Offers ({offers.length})
|
||||
</Heading>
|
||||
)}
|
||||
{offers.map((offer) => (
|
||||
<TranslationOffer key={offer.id} offer={offer} />
|
||||
))}
|
||||
</CardBody>
|
||||
{Object.keys(dvmStatuses).length === 0 && (
|
||||
<Flex gap="2" alignItems="center" m="4">
|
||||
<Spinner />
|
||||
Waiting for offers
|
||||
</Flex>
|
||||
)}
|
||||
<Accordion allowMultiple>
|
||||
{Object.values(dvmStatuses).map((event) => {
|
||||
switch (event.kind) {
|
||||
case DMV_STATUS_KIND:
|
||||
return <TranslationOffer key={event.id} offer={event} />;
|
||||
case DMV_TRANSLATE_RESULT_KIND:
|
||||
return <TranslationResult key={event.id} result={event} />;
|
||||
}
|
||||
})}
|
||||
</Accordion>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@ -115,10 +110,11 @@ function TranslationOffer({ offer }: { offer: NostrEvent }) {
|
||||
|
||||
const [paid, setPaid] = useState(false);
|
||||
const [paying, setPaying] = useState(false);
|
||||
const payInvoice = async () => {
|
||||
const payInvoice: MouseEventHandler = async (e) => {
|
||||
try {
|
||||
if (window.webln && invoice) {
|
||||
setPaying(true);
|
||||
e.stopPropagation();
|
||||
await window.webln.sendPayment(invoice);
|
||||
setPaid(true);
|
||||
}
|
||||
@ -129,27 +125,51 @@ function TranslationOffer({ offer }: { offer: NostrEvent }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex gap="2" direction="column">
|
||||
<Flex gap="2" alignItems="center">
|
||||
<UserAvatarLink pubkey={offer.pubkey} size="sm" />
|
||||
<UserLink pubkey={offer.pubkey} fontWeight="bold" />
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Flex gap="2" alignItems="center" grow={1}>
|
||||
<UserAvatarLink pubkey={offer.pubkey} size="sm" />
|
||||
<UserLink pubkey={offer.pubkey} fontWeight="bold" />
|
||||
<Text>Offered</Text>
|
||||
<Spacer />
|
||||
|
||||
{invoice && amountMsat && (
|
||||
<Button
|
||||
colorScheme="yellow"
|
||||
ml="auto"
|
||||
size="sm"
|
||||
leftIcon={<LightningIcon />}
|
||||
onClick={payInvoice}
|
||||
isLoading={paying || paid}
|
||||
isDisabled={!window.webln}
|
||||
>
|
||||
Pay {readablizeSats(amountMsat / 1000)} sats
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<Text>{offer.content}</Text>
|
||||
</Flex>
|
||||
{invoice && amountMsat && (
|
||||
<Button
|
||||
colorScheme="yellow"
|
||||
size="sm"
|
||||
leftIcon={<LightningIcon />}
|
||||
onClick={payInvoice}
|
||||
isLoading={paying || paid}
|
||||
isDisabled={!window.webln}
|
||||
>
|
||||
Pay {readablizeSats(amountMsat / 1000)} sats
|
||||
</Button>
|
||||
)}
|
||||
<AccordionIcon />
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4}>
|
||||
<Text>{offer.content}</Text>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
}
|
||||
|
||||
function TranslationResult({ result }: { result: NostrEvent }) {
|
||||
return (
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Flex gap="2" alignItems="center" grow={1}>
|
||||
<UserAvatarLink pubkey={result.pubkey} size="sm" />
|
||||
<UserLink pubkey={result.pubkey} fontWeight="bold" />
|
||||
<Text>Translated Note</Text>
|
||||
<AccordionIcon ml="auto" />
|
||||
</Flex>
|
||||
</AccordionButton>
|
||||
<AccordionPanel pb={4}>
|
||||
<NoteContents event={result} />
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
);
|
||||
}
|
||||
|
||||
@ -187,16 +207,12 @@ export default function NoteTranslationModal({
|
||||
}, [requestSignature, note, readRelays]);
|
||||
|
||||
const timeline = useTimelineLoader(`${getEventUID(note)}-translations`, readRelays, {
|
||||
kinds: [DMV_TRANSLATE_JOB_KIND, DMV_TRANSLATE_RESULT_KIND],
|
||||
kinds: [DMV_TRANSLATE_JOB_KIND],
|
||||
"#i": [note.id],
|
||||
});
|
||||
|
||||
const events = useSubject(timeline.timeline);
|
||||
const filteredEvents = events.filter(
|
||||
(e, i, arr) =>
|
||||
e.kind === DMV_TRANSLATE_RESULT_KIND ||
|
||||
(e.kind === DMV_TRANSLATE_JOB_KIND && !arr.some((r) => r.tags.some((t) => isETag(t) && t[1] === e.id))),
|
||||
);
|
||||
const jobs = events.filter((e) => e.kind === DMV_TRANSLATE_JOB_KIND);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl" {...props}>
|
||||
@ -217,16 +233,9 @@ export default function NoteTranslationModal({
|
||||
Request new translation
|
||||
</Button>
|
||||
</Flex>
|
||||
{filteredEvents.map((event) => {
|
||||
switch (event.kind) {
|
||||
case DMV_TRANSLATE_JOB_KIND:
|
||||
return <TranslationRequest key={event.id} request={event} />;
|
||||
case DMV_TRANSLATE_RESULT_KIND:
|
||||
const requestId = event.tags.find(isETag)?.[1];
|
||||
const request = events.find((e) => e.id === requestId);
|
||||
return <TranslationResult key={event.id} result={event} request={request} />;
|
||||
}
|
||||
})}
|
||||
{jobs.map((event) => (
|
||||
<TranslationRequest key={event.id} request={event} />
|
||||
))}
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
25
src/components/note/components/note-proxy-link.tsx
Normal file
25
src/components/note/components/note-proxy-link.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import { IconButton, IconButtonProps, Link } from "@chakra-ui/react";
|
||||
import { ExternalLinkIcon } from "../../icons";
|
||||
import { useMemo } from "react";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
|
||||
export default function NoteProxyLink({
|
||||
event,
|
||||
...props
|
||||
}: Omit<IconButtonProps, "aria-label"> & { event: NostrEvent }) {
|
||||
const externalLink = useMemo(() => event.tags.find((t) => t[0] === "mostr" || t[0] === "proxy"), [event])?.[1];
|
||||
|
||||
if (!externalLink) return null;
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
as={Link}
|
||||
icon={<ExternalLinkIcon />}
|
||||
href={externalLink}
|
||||
target="_blank"
|
||||
aria-label="Open External"
|
||||
title="Open External"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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) => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useMemo, useRef } from "react";
|
||||
import React, { useRef } from "react";
|
||||
import {
|
||||
Box,
|
||||
ButtonGroup,
|
||||
@ -29,7 +29,7 @@ import appSettings from "../../services/settings/app-settings";
|
||||
import EventVerificationIcon from "../event-verification-icon";
|
||||
import { RepostButton } from "./components/repost-button";
|
||||
import { QuoteRepostButton } from "./components/quote-repost-button";
|
||||
import { ExternalLinkIcon, ReplyIcon } from "../icons";
|
||||
import { ReplyIcon } from "../icons";
|
||||
import NoteContentWithWarning from "./note-content-with-warning";
|
||||
import { TrustProvider } from "../../providers/trust";
|
||||
import { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||
@ -47,6 +47,7 @@ import { nip19 } from "nostr-tools";
|
||||
import NoteCommunityMetadata from "./note-community-metadata";
|
||||
import useSingleEvent from "../../hooks/use-single-event";
|
||||
import { InlineNoteContent } from "./inline-note-content";
|
||||
import NoteProxyLink from "./components/note-proxy-link";
|
||||
|
||||
export type NoteProps = Omit<CardProps, "children"> & {
|
||||
event: NostrEvent;
|
||||
@ -79,9 +80,6 @@ export const Note = React.memo(
|
||||
const refs = getReferences(event);
|
||||
const repliedTo = useSingleEvent(refs.replyId);
|
||||
|
||||
// find mostr external link
|
||||
const externalLink = useMemo(() => event.tags.find((t) => t[0] === "mostr" || t[0] === "proxy"), [event])?.[1];
|
||||
|
||||
const showReactionsOnNewLine = useBreakpointValue({ base: true, md: false });
|
||||
|
||||
const reactionButtons = showReactions && <NoteReactions event={event} flexWrap="wrap" variant="ghost" size="xs" />;
|
||||
@ -138,17 +136,7 @@ export const Note = React.memo(
|
||||
</ButtonGroup>
|
||||
{!showReactionsOnNewLine && reactionButtons}
|
||||
<Box flexGrow={1} />
|
||||
{externalLink && (
|
||||
<IconButton
|
||||
as={Link}
|
||||
icon={<ExternalLinkIcon />}
|
||||
aria-label="Open External"
|
||||
href={externalLink}
|
||||
size="xs"
|
||||
variant="ghost"
|
||||
target="_blank"
|
||||
/>
|
||||
)}
|
||||
<NoteProxyLink event={event} size="xs" variant="ghost" />
|
||||
<EventRelays event={event} />
|
||||
<BookmarkButton event={event} aria-label="Bookmark note" size="xs" variant="ghost" />
|
||||
<NoteMenu event={event} size="xs" variant="ghost" aria-label="More Options" />
|
||||
|
@ -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 (
|
||||
<MenuItem onClick={togglePin} icon={<PinIcon />} isDisabled={loading || account.readonly}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Omit<MenuIconButtonProps, "children">) {
|
||||
const account = useCurrentAccount();
|
||||
@ -90,6 +134,7 @@ export default function NoteMenu({ event, ...props }: { event: NostrEvent } & Om
|
||||
<MenuItem onClick={broadcast} icon={<BroadcastEventIcon />}>
|
||||
Broadcast
|
||||
</MenuItem>
|
||||
<PinNoteItem event={event} />
|
||||
<MenuItem onClick={infoModal.onOpen} icon={<CodeIcon />}>
|
||||
View Raw
|
||||
</MenuItem>
|
||||
|
@ -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<HTMLSelectElement, Omit<SelectProps, "children">>(({ ...props }, ref) => {
|
||||
const account = useCurrentAccount();
|
||||
const { pointers } = useJoinedCommunitiesList(account?.pubkey);
|
||||
const { pointers } = useUserCommunitiesList(account?.pubkey);
|
||||
|
||||
return (
|
||||
<Select placeholder="Select community" {...props} ref={ref}>
|
||||
|
@ -4,7 +4,7 @@ import { RequestOptions } from "../services/replaceable-event-requester";
|
||||
import useCurrentAccount from "./use-current-account";
|
||||
import useReplaceableEvent from "./use-replaceable-event";
|
||||
|
||||
export default function useJoinedCommunitiesList(pubkey?: string, opts?: RequestOptions) {
|
||||
export default function useUserCommunitiesList(pubkey?: string, relays: string[] = [], opts?: RequestOptions) {
|
||||
const account = useCurrentAccount();
|
||||
const key = pubkey ?? account?.pubkey;
|
||||
|
||||
@ -22,10 +22,9 @@ export default function useJoinedCommunitiesList(pubkey?: string, opts?: Request
|
||||
[],
|
||||
opts,
|
||||
);
|
||||
const list = useReplaceableEvent(key ? { kind: COMMUNITIES_LIST_KIND, pubkey: key } : undefined, [], opts);
|
||||
const list = useReplaceableEvent(key ? { kind: COMMUNITIES_LIST_KIND, pubkey: key } : undefined, relays, opts);
|
||||
|
||||
let useList = list || oldList;
|
||||
console.log(list, oldList);
|
||||
|
||||
// if both exist, use the newest one
|
||||
if (list && oldList) {
|
15
src/hooks/use-user-pin-list.ts
Normal file
15
src/hooks/use-user-pin-list.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { PIN_LIST_KIND, getEventsFromList } from "../helpers/nostr/lists";
|
||||
import { RequestOptions } from "../services/replaceable-event-requester";
|
||||
import useCurrentAccount from "./use-current-account";
|
||||
import useReplaceableEvent from "./use-replaceable-event";
|
||||
|
||||
export default function useUserPinList(pubkey?: string, relays: string[] = [], opts?: RequestOptions) {
|
||||
const account = useCurrentAccount();
|
||||
const key = pubkey ?? account?.pubkey;
|
||||
|
||||
const list = useReplaceableEvent(key ? { kind: PIN_LIST_KIND, pubkey: key } : undefined, relays, opts);
|
||||
|
||||
const events = list ? getEventsFromList(list) : [];
|
||||
|
||||
return { list, events };
|
||||
}
|
@ -57,15 +57,23 @@ class EventExistsService {
|
||||
const nextRelay = relayScoreboardService.getRankedRelays(Array.from(relays))[0];
|
||||
|
||||
if (!nextRelay) continue;
|
||||
relays.delete(nextRelay);
|
||||
|
||||
(async () => {
|
||||
const sub = this.answers.get(key);
|
||||
const request = new NostrRequest([nextRelay], 500);
|
||||
const limitFilter = Array.isArray(filter) ? filter.map((f) => ({ ...f, limit: 1 })) : { ...filter, limit: 1 };
|
||||
request.start(limitFilter);
|
||||
request.onEvent.subscribe(() => sub.next(true));
|
||||
request.onEvent.subscribe(() => {
|
||||
this.log("Found event for", filter);
|
||||
sub.next(true);
|
||||
this.pending.delete(key);
|
||||
});
|
||||
await request.onComplete;
|
||||
if (sub.value === undefined) sub.next(false);
|
||||
if (sub.value === undefined && this.asked.get(key).size > this.pending.get(key).size) {
|
||||
this.log("Could not find event for", filter);
|
||||
sub.next(false);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import dayjs from "dayjs";
|
||||
import { Button, ButtonProps, useToast } from "@chakra-ui/react";
|
||||
|
||||
import { DraftNostrEvent, NostrEvent, isDTag } from "../../../types/nostr-event";
|
||||
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 { COMMUNITIES_LIST_KIND, listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists";
|
||||
@ -16,10 +16,10 @@ export default function CommunityJoinButton({
|
||||
community,
|
||||
...props
|
||||
}: Omit<ButtonProps, "children"> & { community: NostrEvent }) {
|
||||
const account = useCurrentAccount();
|
||||
const { list, pointers } = useJoinedCommunitiesList(account?.pubkey);
|
||||
const { requestSignature } = useSigningContext();
|
||||
const toast = useToast();
|
||||
const account = useCurrentAccount();
|
||||
const { list, pointers } = useUserCommunitiesList(account?.pubkey);
|
||||
const { requestSignature } = useSigningContext();
|
||||
|
||||
const isSubscribed = pointers.find(
|
||||
(cord) => cord.identifier === getCommunityName(community) && cord.pubkey === community.pubkey,
|
||||
@ -29,7 +29,7 @@ export default function CommunityJoinButton({
|
||||
try {
|
||||
const favList = {
|
||||
kind: COMMUNITIES_LIST_KIND,
|
||||
content: "",
|
||||
content: list?.content ?? "",
|
||||
created_at: dayjs().unix(),
|
||||
tags: list?.tags.filter((t) => !isDTag(t)) ?? [],
|
||||
};
|
||||
|
@ -25,7 +25,7 @@ import dayjs from "dayjs";
|
||||
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import useJoinedCommunitiesList from "../../hooks/use-communities-joined-list";
|
||||
import useUserCommunitiesList from "../../hooks/use-user-communities-list";
|
||||
import useCurrentAccount from "../../hooks/use-current-account";
|
||||
import CommunityCard from "./components/community-card";
|
||||
import CommunityCreateModal, { FormValues } from "./components/community-create-modal";
|
||||
@ -62,7 +62,9 @@ function CommunitiesHomePage() {
|
||||
const createModal = useDisclosure();
|
||||
|
||||
const readRelays = useReadRelayUrls();
|
||||
const { pointers: communityCoordinates } = useJoinedCommunitiesList(account.pubkey, { alwaysRequest: true });
|
||||
const { pointers: communityCoordinates } = useUserCommunitiesList(account.pubkey, readRelays, {
|
||||
alwaysRequest: true,
|
||||
});
|
||||
const communities = useReplaceableEvents(communityCoordinates, readRelays).sort(
|
||||
(a, b) => b.created_at - a.created_at,
|
||||
);
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
Flex,
|
||||
IconButton,
|
||||
Link,
|
||||
Spacer,
|
||||
useColorMode,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
@ -34,6 +35,8 @@ import { useBreakpointValue } from "../../../providers/breakpoint-provider";
|
||||
import NoteReactions from "../../../components/note/components/note-reactions";
|
||||
import BookmarkButton from "../../../components/note/components/bookmark-button";
|
||||
import NoteCommunityMetadata from "../../../components/note/note-community-metadata";
|
||||
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
|
||||
import NoteProxyLink from "../../../components/note/components/note-proxy-link";
|
||||
|
||||
const LEVEL_COLORS = ["green", "blue", "red", "purple", "yellow", "cyan", "pink"];
|
||||
|
||||
@ -78,6 +81,7 @@ export const ThreadPost = ({ post, initShowReplies, focusId, level = -1 }: Threa
|
||||
<Flex gap="2" alignItems="center">
|
||||
<UserAvatarLink pubkey={post.event.pubkey} size="sm" />
|
||||
<UserLink pubkey={post.event.pubkey} fontWeight="bold" isTruncated />
|
||||
<UserDnsIdentityIcon pubkey={post.event.pubkey} onlyIcon />
|
||||
<Link as={RouterLink} whiteSpace="nowrap" color="current" to={`/n/${nip19.noteEncode(post.event.id)}`}>
|
||||
<Timestamp timestamp={post.event.created_at} />
|
||||
</Link>
|
||||
@ -124,7 +128,9 @@ export const ThreadPost = ({ post, initShowReplies, focusId, level = -1 }: Threa
|
||||
<NoteZapButton event={post.event} />
|
||||
</ButtonGroup>
|
||||
{!showReactionsOnNewLine && reactionButtons}
|
||||
<BookmarkButton event={post.event} variant="ghost" aria-label="Bookmark" size="sm" ml="auto" />
|
||||
<Spacer />
|
||||
<NoteProxyLink event={post.event} variant="ghost" size="sm" />
|
||||
<BookmarkButton event={post.event} variant="ghost" aria-label="Bookmark" size="sm" />
|
||||
<NoteMenu event={post.event} variant="ghost" size="sm" aria-label="More Options" />
|
||||
</Flex>
|
||||
);
|
||||
|
@ -1,35 +1,11 @@
|
||||
import { useOutletContext, Link as RouterLink } from "react-router-dom";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
Heading,
|
||||
IconButton,
|
||||
Image,
|
||||
Link,
|
||||
SimpleGrid,
|
||||
Stat,
|
||||
StatGroup,
|
||||
StatHelpText,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from "@chakra-ui/react";
|
||||
import { useAsync } from "react-use";
|
||||
import { Kind, nip19 } from "nostr-tools";
|
||||
import { Box, Button, Flex, Heading, IconButton, Image, Link, Text, useDisclosure } from "@chakra-ui/react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import { readablizeSats } from "../../../helpers/bolt11";
|
||||
import { getUserDisplayName } from "../../../helpers/user-metadata";
|
||||
import { getLudEndpoint } from "../../../helpers/lnurl";
|
||||
import { EmbedableContent, embedUrls } from "../../../helpers/embeds";
|
||||
import { truncatedId } from "../../../helpers/nostr/events";
|
||||
import trustedUserStatsService from "../../../services/trusted-user-stats";
|
||||
import { parseAddress } from "../../../services/dns-identity";
|
||||
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
|
||||
import { useUserMetadata } from "../../../hooks/use-user-metadata";
|
||||
@ -51,15 +27,10 @@ import { UserFollowButton } from "../../../components/user-follow-button";
|
||||
import UserZapButton from "../components/user-zap-button";
|
||||
import { UserProfileMenu } from "../components/user-profile-menu";
|
||||
import { useSharableProfileId } from "../../../hooks/use-shareable-profile-id";
|
||||
import useUserContactList from "../../../hooks/use-user-contact-list";
|
||||
import { getPubkeysFromList } from "../../../helpers/nostr/lists";
|
||||
import Timestamp from "../../../components/timestamp";
|
||||
import UserProfileBadges from "./user-profile-badges";
|
||||
import useEventCount from "../../../hooks/use-event-count";
|
||||
import useJoinedCommunitiesList from "../../../hooks/use-communities-joined-list";
|
||||
import { PointerCommunityCard } from "../../communities/components/community-card";
|
||||
import { ErrorBoundary } from "../../../components/error-boundary";
|
||||
import { useBreakpointValue } from "../../../providers/breakpoint-provider";
|
||||
import UserJoinedCommunities from "./user-joined-communities";
|
||||
import UserPinnedEvents from "./user-pinned-events";
|
||||
import UserStatsAccordion from "./user-stats-accordion";
|
||||
|
||||
function buildDescriptionContent(description: string) {
|
||||
let content: EmbedableContent = [description.trim()];
|
||||
@ -70,153 +41,6 @@ function buildDescriptionContent(description: string) {
|
||||
return content;
|
||||
}
|
||||
|
||||
function UserStatsAccordion({ pubkey }: { pubkey: string }) {
|
||||
const contextRelays = useAdditionalRelayContext();
|
||||
const contacts = useUserContactList(pubkey, contextRelays);
|
||||
|
||||
const { value: stats } = useAsync(() => trustedUserStatsService.getUserStats(pubkey), [pubkey]);
|
||||
const followerCount = useEventCount({ "#p": [pubkey], kinds: [Kind.Contacts] });
|
||||
|
||||
return (
|
||||
<Accordion allowMultiple>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Network Stats
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb="2">
|
||||
<StatGroup gap="4" whiteSpace="pre">
|
||||
<Stat>
|
||||
<StatLabel>Following</StatLabel>
|
||||
<StatNumber>{contacts ? readablizeSats(getPubkeysFromList(contacts).length) : "Unknown"}</StatNumber>
|
||||
{contacts && (
|
||||
<StatHelpText>
|
||||
Updated <Timestamp timestamp={contacts.created_at} />
|
||||
</StatHelpText>
|
||||
)}
|
||||
</Stat>
|
||||
|
||||
{stats && (
|
||||
<>
|
||||
<Stat>
|
||||
<StatLabel>Followers</StatLabel>
|
||||
<StatNumber>{readablizeSats(followerCount ?? 0) || 0}</StatNumber>
|
||||
</Stat>
|
||||
|
||||
<Stat>
|
||||
<StatLabel>Notes & replies</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.pub_note_count) || 0}</StatNumber>
|
||||
</Stat>
|
||||
|
||||
<Stat>
|
||||
<StatLabel>Reactions</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.pub_reaction_count) || 0}</StatNumber>
|
||||
</Stat>
|
||||
</>
|
||||
)}
|
||||
</StatGroup>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
{(stats?.zaps_sent || stats?.zaps_received) && (
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Zap Stats
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb="2">
|
||||
<StatGroup gap="4" whiteSpace="pre">
|
||||
{stats.zaps_sent && (
|
||||
<>
|
||||
<Stat>
|
||||
<StatLabel>Zap Sent</StatLabel>
|
||||
<StatNumber>{stats.zaps_sent.count}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Total Sats Sent</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.zaps_sent.msats / 1000)}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Avg Zap Sent</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.zaps_sent.avg_msats / 1000)}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Biggest Zap Sent</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.zaps_sent.max_msats / 1000)}</StatNumber>
|
||||
</Stat>
|
||||
</>
|
||||
)}
|
||||
|
||||
{stats.zaps_received && (
|
||||
<>
|
||||
<Stat>
|
||||
<StatLabel>Zap Received</StatLabel>
|
||||
<StatNumber>{stats.zaps_received.count}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Total Sats Received</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.zaps_received.msats / 1000)}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Avg Zap Received</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.zaps_received.avg_msats / 1000)}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Biggest Zap Received</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.zaps_received.max_msats / 1000)}</StatNumber>
|
||||
</Stat>
|
||||
</>
|
||||
)}
|
||||
</StatGroup>
|
||||
<Text color="slategrey">
|
||||
Stats from{" "}
|
||||
<Link href="https://nostr.band" isExternal color="blue.500">
|
||||
nostr.band
|
||||
</Link>
|
||||
</Text>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
function UserJoinedCommunities({ pubkey }: { pubkey: string }) {
|
||||
const { pointers: communities } = useJoinedCommunitiesList(pubkey);
|
||||
const columns = useBreakpointValue({ base: 1, lg: 2, xl: 3 }) ?? 1;
|
||||
const showAllCommunities = useDisclosure();
|
||||
|
||||
if (communities.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Flex direction="column" px="2">
|
||||
<Heading size="md" my="2">
|
||||
Joined Communities ({communities.length})
|
||||
</Heading>
|
||||
<SimpleGrid spacing="4" columns={columns}>
|
||||
{(showAllCommunities.isOpen ? communities : communities.slice(0, columns * 2)).map((pointer) => (
|
||||
<ErrorBoundary key={pointer.identifier + pointer.pubkey}>
|
||||
<PointerCommunityCard pointer={pointer} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
{!showAllCommunities.isOpen && communities.length > columns * 2 && (
|
||||
<Button variant="link" py="4" onClick={showAllCommunities.onOpen}>
|
||||
Show All
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
export default function UserAboutTab() {
|
||||
const expanded = useDisclosure();
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
@ -366,6 +190,7 @@ export default function UserAboutTab() {
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<UserPinnedEvents pubkey={pubkey} />
|
||||
<UserJoinedCommunities pubkey={pubkey} />
|
||||
</Flex>
|
||||
);
|
||||
|
36
src/views/user/about/user-joined-communities.tsx
Normal file
36
src/views/user/about/user-joined-communities.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { Button, Flex, Heading, SimpleGrid, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
|
||||
import useUserCommunitiesList from "../../../hooks/use-user-communities-list";
|
||||
import { PointerCommunityCard } from "../../communities/components/community-card";
|
||||
import { ErrorBoundary } from "../../../components/error-boundary";
|
||||
import { useBreakpointValue } from "../../../providers/breakpoint-provider";
|
||||
|
||||
export default function UserJoinedCommunities({ pubkey }: { pubkey: string }) {
|
||||
const contextRelays = useAdditionalRelayContext();
|
||||
const { pointers: communities } = useUserCommunitiesList(pubkey, contextRelays, { alwaysRequest: true });
|
||||
const columns = useBreakpointValue({ base: 1, lg: 2, xl: 3 }) ?? 1;
|
||||
const showAllCommunities = useDisclosure();
|
||||
|
||||
if (communities.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Flex direction="column" px="2">
|
||||
<Heading size="md" my="2">
|
||||
Joined Communities ({communities.length})
|
||||
</Heading>
|
||||
<SimpleGrid spacing="4" columns={columns}>
|
||||
{(showAllCommunities.isOpen ? communities : communities.slice(0, columns * 2)).map((pointer) => (
|
||||
<ErrorBoundary key={pointer.identifier + pointer.pubkey}>
|
||||
<PointerCommunityCard pointer={pointer} />
|
||||
</ErrorBoundary>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
{!showAllCommunities.isOpen && communities.length > columns * 2 && (
|
||||
<Button variant="link" pt="4" onClick={showAllCommunities.onOpen}>
|
||||
Show All
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
32
src/views/user/about/user-pinned-events.tsx
Normal file
32
src/views/user/about/user-pinned-events.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { Button, Flex, Heading, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
|
||||
import useUserPinList from "../../../hooks/use-user-pin-list";
|
||||
import { EmbedEventPointer } from "../../../components/embed-event";
|
||||
|
||||
export default function UserPinnedEvents({ pubkey }: { pubkey: string }) {
|
||||
const contextRelays = useAdditionalRelayContext();
|
||||
const { events, list } = useUserPinList(pubkey, contextRelays);
|
||||
const showAll = useDisclosure();
|
||||
|
||||
if (events.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Flex direction="column" gap="2">
|
||||
<Heading size="md" my="2">
|
||||
Pinned
|
||||
</Heading>
|
||||
{(showAll.isOpen ? events : events.slice(0, 2)).map((event) => (
|
||||
<EmbedEventPointer
|
||||
key={event.id}
|
||||
pointer={{ type: "nevent", data: { id: event.id, relays: event.relay ? [event.relay] : [] } }}
|
||||
/>
|
||||
))}
|
||||
{!showAll.isOpen && events.length > 2 && (
|
||||
<Button variant="link" pt="4" onClick={showAll.onOpen}>
|
||||
Show All
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
144
src/views/user/about/user-stats-accordion.tsx
Normal file
144
src/views/user/about/user-stats-accordion.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Link,
|
||||
Stat,
|
||||
StatGroup,
|
||||
StatHelpText,
|
||||
StatLabel,
|
||||
StatNumber,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { useAsync } from "react-use";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
import { readablizeSats } from "../../../helpers/bolt11";
|
||||
import trustedUserStatsService from "../../../services/trusted-user-stats";
|
||||
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
|
||||
import useUserContactList from "../../../hooks/use-user-contact-list";
|
||||
import { getPubkeysFromList } from "../../../helpers/nostr/lists";
|
||||
import Timestamp from "../../../components/timestamp";
|
||||
import useEventCount from "../../../hooks/use-event-count";
|
||||
|
||||
export default function UserStatsAccordion({ pubkey }: { pubkey: string }) {
|
||||
const contextRelays = useAdditionalRelayContext();
|
||||
const contacts = useUserContactList(pubkey, contextRelays);
|
||||
|
||||
const { value: stats } = useAsync(() => trustedUserStatsService.getUserStats(pubkey), [pubkey]);
|
||||
const followerCount = useEventCount({ "#p": [pubkey], kinds: [Kind.Contacts] });
|
||||
|
||||
return (
|
||||
<Accordion allowMultiple>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Network Stats
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb="2">
|
||||
<StatGroup gap="4" whiteSpace="pre">
|
||||
<Stat>
|
||||
<StatLabel>Following</StatLabel>
|
||||
<StatNumber>{contacts ? readablizeSats(getPubkeysFromList(contacts).length) : "Unknown"}</StatNumber>
|
||||
{contacts && (
|
||||
<StatHelpText>
|
||||
Updated <Timestamp timestamp={contacts.created_at} />
|
||||
</StatHelpText>
|
||||
)}
|
||||
</Stat>
|
||||
|
||||
{stats && (
|
||||
<>
|
||||
<Stat>
|
||||
<StatLabel>Followers</StatLabel>
|
||||
<StatNumber>{readablizeSats(followerCount ?? 0) || 0}</StatNumber>
|
||||
</Stat>
|
||||
|
||||
<Stat>
|
||||
<StatLabel>Notes & replies</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.pub_note_count) || 0}</StatNumber>
|
||||
</Stat>
|
||||
|
||||
<Stat>
|
||||
<StatLabel>Reactions</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.pub_reaction_count) || 0}</StatNumber>
|
||||
</Stat>
|
||||
</>
|
||||
)}
|
||||
</StatGroup>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
{(stats?.zaps_sent || stats?.zaps_received) && (
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Zap Stats
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb="2">
|
||||
<StatGroup gap="4" whiteSpace="pre">
|
||||
{stats.zaps_sent && (
|
||||
<>
|
||||
<Stat>
|
||||
<StatLabel>Zap Sent</StatLabel>
|
||||
<StatNumber>{stats.zaps_sent.count}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Total Sats Sent</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.zaps_sent.msats / 1000)}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Avg Zap Sent</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.zaps_sent.avg_msats / 1000)}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Biggest Zap Sent</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.zaps_sent.max_msats / 1000)}</StatNumber>
|
||||
</Stat>
|
||||
</>
|
||||
)}
|
||||
|
||||
{stats.zaps_received && (
|
||||
<>
|
||||
<Stat>
|
||||
<StatLabel>Zap Received</StatLabel>
|
||||
<StatNumber>{stats.zaps_received.count}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Total Sats Received</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.zaps_received.msats / 1000)}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Avg Zap Received</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.zaps_received.avg_msats / 1000)}</StatNumber>
|
||||
</Stat>
|
||||
<Stat>
|
||||
<StatLabel>Biggest Zap Received</StatLabel>
|
||||
<StatNumber>{readablizeSats(stats.zaps_received.max_msats / 1000)}</StatNumber>
|
||||
</Stat>
|
||||
</>
|
||||
)}
|
||||
</StatGroup>
|
||||
<Text color="slategrey">
|
||||
Stats from{" "}
|
||||
<Link href="https://nostr.band" isExternal color="blue.500">
|
||||
nostr.band
|
||||
</Link>
|
||||
</Text>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
)}
|
||||
</Accordion>
|
||||
);
|
||||
}
|
@ -109,9 +109,9 @@ const UserView = () => {
|
||||
]);
|
||||
const hasArticles = useEventExists({ kinds: [Kind.Article], authors: [pubkey] }, readRelays);
|
||||
const hasStreams = useEventExists({ kinds: [STREAM_KIND], authors: [pubkey] }, [
|
||||
"wss://relay.snort.social",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.damus.io",
|
||||
"wss://relay.snort.social",
|
||||
"wss://nostr.wine",
|
||||
...readRelays,
|
||||
]);
|
||||
@ -124,7 +124,7 @@ const UserView = () => {
|
||||
if (tab.path === "streams" && hasStreams === false) return false;
|
||||
return true;
|
||||
}),
|
||||
[hasTracks, hasArticles, tabs],
|
||||
[hasTracks, hasArticles, hasStreams, tabs],
|
||||
);
|
||||
|
||||
const matches = useMatches();
|
||||
|
@ -3,15 +3,16 @@ import react from "@vitejs/plugin-react";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
|
||||
const isProd = process.env.NODE_ENV === "production";
|
||||
process.env.VITE_ANALYTICS_SCRIPT = isProd
|
||||
? `
|
||||
process.env.VITE_ANALYTICS_SCRIPT =
|
||||
isProd && process.env.ACKEE_DOMAIN_ID
|
||||
? `
|
||||
<script
|
||||
async defer
|
||||
src="//ackee.nostrudel.ninja/tracker.js"
|
||||
data-ackee-server="//ackee.nostrudel.ninja"
|
||||
data-ackee-domain-id="58b1c39f-43f9-422b-bc7d-06aff35e764e"
|
||||
data-ackee-domain-id="${process.env.ACKEE_DOMAIN_ID}"
|
||||
></script>`
|
||||
: "";
|
||||
: "";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
|
Loading…
x
Reference in New Issue
Block a user