mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-29 04:52:59 +02:00
add vote buttons to wiki pages
This commit is contained in:
@@ -40,9 +40,7 @@ export default function EmbeddedWikiPage({ page: page, ...props }: Omit<CardProp
|
|||||||
</Text>
|
</Text>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody p="2" overflow="hidden">
|
<CardBody p="2" overflow="hidden">
|
||||||
<Text color="GrayText" noOfLines={2}>
|
<Text noOfLines={2}>{getPageSummary(page)}</Text>
|
||||||
{getPageSummary(page)}
|
|
||||||
</Text>
|
|
||||||
</CardBody>
|
</CardBody>
|
||||||
{showFooter && (
|
{showFooter && (
|
||||||
<CardFooter>
|
<CardFooter>
|
||||||
|
80
src/components/reactions/event-vote-buttions.tsx
Normal file
80
src/components/reactions/event-vote-buttions.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { useCallback, useMemo, useState } from "react";
|
||||||
|
import { Flex, FlexProps, IconButton, IconButtonProps, Text } from "@chakra-ui/react";
|
||||||
|
|
||||||
|
import { ChevronDownIcon, ChevronUpIcon, DislikeIcon, LikeIcon } from "../icons";
|
||||||
|
import useCurrentAccount from "../../hooks/use-current-account";
|
||||||
|
import useEventReactions from "../../hooks/use-event-reactions";
|
||||||
|
import { draftEventReaction, getEventReactionScore, groupReactions } from "../../helpers/nostr/reactions";
|
||||||
|
import { NostrEvent } from "../../types/nostr-event";
|
||||||
|
import { useAdditionalRelayContext } from "../../providers/local/additional-relay-context";
|
||||||
|
import { usePublishEvent } from "../../providers/global/publish-provider";
|
||||||
|
|
||||||
|
export default function EventVoteButtons({
|
||||||
|
event,
|
||||||
|
chevrons = true,
|
||||||
|
inline = false,
|
||||||
|
variant = "ghost",
|
||||||
|
...props
|
||||||
|
}: Omit<FlexProps, "children"> & {
|
||||||
|
event: NostrEvent;
|
||||||
|
chevrons?: boolean;
|
||||||
|
inline?: boolean;
|
||||||
|
variant?: IconButtonProps["variant"];
|
||||||
|
}) {
|
||||||
|
const account = useCurrentAccount();
|
||||||
|
const publish = usePublishEvent();
|
||||||
|
const reactions = useEventReactions(event.id);
|
||||||
|
const additionalRelays = useAdditionalRelayContext();
|
||||||
|
|
||||||
|
const grouped = useMemo(() => groupReactions(reactions ?? []), [reactions]);
|
||||||
|
const { vote, up, down } = getEventReactionScore(grouped);
|
||||||
|
|
||||||
|
const hasUpVote = !!account && !!up?.pubkeys.includes(account.pubkey);
|
||||||
|
const hasDownVote = !!account && !!down?.pubkeys.includes(account.pubkey);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const addVote = useCallback(
|
||||||
|
async (vote: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
const draft = draftEventReaction(event, vote);
|
||||||
|
await publish("Reaction", draft, additionalRelays);
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
[event, publish, additionalRelays],
|
||||||
|
);
|
||||||
|
|
||||||
|
const upIcon = chevrons ? <ChevronUpIcon boxSize={6} /> : <LikeIcon />;
|
||||||
|
const downIcon = chevrons ? <ChevronDownIcon boxSize={6} /> : <DislikeIcon />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDirection={inline ? "row" : "column"} alignItems="center" {...props}>
|
||||||
|
<IconButton
|
||||||
|
aria-label="up vote"
|
||||||
|
title="up vote"
|
||||||
|
icon={upIcon}
|
||||||
|
size="sm"
|
||||||
|
variant={hasUpVote ? "solid" : variant}
|
||||||
|
isLoading={loading}
|
||||||
|
onClick={() => addVote("+")}
|
||||||
|
isDisabled={!account || !!hasUpVote || !!hasDownVote}
|
||||||
|
colorScheme={hasUpVote ? "primary" : "gray"}
|
||||||
|
/>
|
||||||
|
{(up || down) && vote > 0 && (
|
||||||
|
<Text p="2" lineHeight="1em">
|
||||||
|
{vote}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
<IconButton
|
||||||
|
aria-label="down vote"
|
||||||
|
title="down vote"
|
||||||
|
icon={downIcon}
|
||||||
|
size="sm"
|
||||||
|
variant={hasDownVote ? "solid" : variant}
|
||||||
|
isLoading={loading}
|
||||||
|
onClick={() => addVote("-")}
|
||||||
|
isDisabled={!account || !!hasUpVote || !!hasDownVote}
|
||||||
|
colorScheme={hasDownVote ? "primary" : "gray"}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
@@ -85,13 +85,6 @@ export function buildApprovalMap(events: Iterable<NostrEvent>, mods: string[]) {
|
|||||||
return approvals;
|
return approvals;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCommunityPostVote(grouped: ReactionGroup[]) {
|
|
||||||
const up = grouped.find((r) => r.emoji === "+");
|
|
||||||
const down = grouped.find((r) => r.emoji === "-");
|
|
||||||
const vote = (up?.pubkeys.length ?? 0) - (down?.pubkeys.length ?? 0);
|
|
||||||
return { up, down, vote };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEventCommunityPointer(event: NostrEvent) {
|
export function getEventCommunityPointer(event: NostrEvent) {
|
||||||
const communityTag = event.tags.filter(isATag).find((t) => t[1].startsWith(COMMUNITY_DEFINITION_KIND + ":"));
|
const communityTag = event.tags.filter(isATag).find((t) => t[1].startsWith(COMMUNITY_DEFINITION_KIND + ":"));
|
||||||
return communityTag ? parseCoordinate(communityTag[1], true) : null;
|
return communityTag ? parseCoordinate(communityTag[1], true) : null;
|
||||||
|
@@ -41,3 +41,10 @@ export function draftEventReaction(event: NostrEvent, emoji = "+", url?: string)
|
|||||||
|
|
||||||
return draft;
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getEventReactionScore(grouped: ReactionGroup[]) {
|
||||||
|
const up = grouped.find((r) => r.emoji === "+");
|
||||||
|
const down = grouped.find((r) => r.emoji === "-");
|
||||||
|
const vote = (up?.pubkeys.length ?? 0) - (down?.pubkeys.length ?? 0);
|
||||||
|
return { up, down, vote };
|
||||||
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { Flex } from "@chakra-ui/react";
|
import { Card, Flex } from "@chakra-ui/react";
|
||||||
|
|
||||||
import PostVoteButtons from "./post-vote-buttions";
|
import EventVoteButtons from "../../../components/reactions/event-vote-buttions";
|
||||||
import CommunityPost from "./community-post";
|
import CommunityPost from "./community-post";
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { NostrEvent } from "../../../types/nostr-event";
|
||||||
|
|
||||||
@@ -9,7 +9,9 @@ const ApprovedEvent = memo(
|
|||||||
({ event, approvals, showCommunity }: { event: NostrEvent; approvals: NostrEvent[]; showCommunity?: boolean }) => {
|
({ event, approvals, showCommunity }: { event: NostrEvent; approvals: NostrEvent[]; showCommunity?: boolean }) => {
|
||||||
return (
|
return (
|
||||||
<Flex gap="2" alignItems="flex-start">
|
<Flex gap="2" alignItems="flex-start">
|
||||||
<PostVoteButtons event={event} flexShrink={0} />
|
<Card borderRadius="lg">
|
||||||
|
<EventVoteButtons event={event} flexShrink={0} />
|
||||||
|
</Card>
|
||||||
<CommunityPost event={event} approvals={approvals} flex={1} showCommunity={showCommunity} />
|
<CommunityPost event={event} approvals={approvals} flex={1} showCommunity={showCommunity} />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
@@ -1,63 +0,0 @@
|
|||||||
import { useCallback, useMemo, useState } from "react";
|
|
||||||
import { Card, CardProps, IconButton, Text } from "@chakra-ui/react";
|
|
||||||
|
|
||||||
import useCurrentAccount from "../../../hooks/use-current-account";
|
|
||||||
import useEventReactions from "../../../hooks/use-event-reactions";
|
|
||||||
import { draftEventReaction, groupReactions } from "../../../helpers/nostr/reactions";
|
|
||||||
import { getCommunityPostVote } from "../../../helpers/nostr/communities";
|
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from "../../../components/icons";
|
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
|
||||||
import { useAdditionalRelayContext } from "../../../providers/local/additional-relay-context";
|
|
||||||
import { usePublishEvent } from "../../../providers/global/publish-provider";
|
|
||||||
|
|
||||||
export default function PostVoteButtons({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
|
|
||||||
const account = useCurrentAccount();
|
|
||||||
const publish = usePublishEvent();
|
|
||||||
const reactions = useEventReactions(event.id);
|
|
||||||
const additionalRelays = useAdditionalRelayContext();
|
|
||||||
|
|
||||||
const grouped = useMemo(() => groupReactions(reactions ?? []), [reactions]);
|
|
||||||
const { vote, up, down } = getCommunityPostVote(grouped);
|
|
||||||
|
|
||||||
const hasUpVote = !!account && !!up?.pubkeys.includes(account.pubkey);
|
|
||||||
const hasDownVote = !!account && !!down?.pubkeys.includes(account.pubkey);
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const addVote = useCallback(
|
|
||||||
async (vote: string) => {
|
|
||||||
setLoading(true);
|
|
||||||
const draft = draftEventReaction(event, vote);
|
|
||||||
await publish("Reaction", draft, additionalRelays);
|
|
||||||
setLoading(false);
|
|
||||||
},
|
|
||||||
[event, publish, additionalRelays],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card direction="column" alignItems="center" borderRadius="lg" {...props}>
|
|
||||||
<IconButton
|
|
||||||
aria-label="up vote"
|
|
||||||
title="up vote"
|
|
||||||
icon={<ChevronUpIcon boxSize={6} />}
|
|
||||||
size="sm"
|
|
||||||
variant={hasUpVote ? "solid" : "ghost"}
|
|
||||||
isLoading={loading}
|
|
||||||
onClick={() => addVote("+")}
|
|
||||||
isDisabled={!account || !!hasUpVote || !!hasDownVote}
|
|
||||||
colorScheme={hasUpVote ? "primary" : "gray"}
|
|
||||||
/>
|
|
||||||
{(up || down) && <Text my="1">{vote}</Text>}
|
|
||||||
<IconButton
|
|
||||||
aria-label="down vote"
|
|
||||||
title="down vote"
|
|
||||||
icon={<ChevronDownIcon boxSize={6} />}
|
|
||||||
size="sm"
|
|
||||||
variant={hasDownVote ? "solid" : "ghost"}
|
|
||||||
isLoading={loading}
|
|
||||||
onClick={() => addVote("-")}
|
|
||||||
isDisabled={!account || !!hasUpVote || !!hasDownVote}
|
|
||||||
colorScheme={hasDownVote ? "primary" : "gray"}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -5,7 +5,6 @@ import {
|
|||||||
COMMUNITY_APPROVAL_KIND,
|
COMMUNITY_APPROVAL_KIND,
|
||||||
buildApprovalMap,
|
buildApprovalMap,
|
||||||
getCommunityMods,
|
getCommunityMods,
|
||||||
getCommunityPostVote,
|
|
||||||
getCommunityRelays,
|
getCommunityRelays,
|
||||||
} from "../../../helpers/nostr/communities";
|
} from "../../../helpers/nostr/communities";
|
||||||
import useSubject from "../../../hooks/use-subject";
|
import useSubject from "../../../hooks/use-subject";
|
||||||
@@ -14,7 +13,7 @@ import IntersectionObserverProvider from "../../../providers/local/intersection-
|
|||||||
import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status";
|
import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status";
|
||||||
import useUserMuteFilter from "../../../hooks/use-user-mute-filter";
|
import useUserMuteFilter from "../../../hooks/use-user-mute-filter";
|
||||||
import useEventsReactions from "../../../hooks/use-events-reactions";
|
import useEventsReactions from "../../../hooks/use-events-reactions";
|
||||||
import { groupReactions } from "../../../helpers/nostr/reactions";
|
import { getEventReactionScore, groupReactions } from "../../../helpers/nostr/reactions";
|
||||||
import ApprovedEvent from "../components/community-approved-post";
|
import ApprovedEvent from "../components/community-approved-post";
|
||||||
import { RouterContext } from "../community-home";
|
import { RouterContext } from "../community-home";
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ export default function CommunityTrendingView() {
|
|||||||
const dir: Record<string, number> = {};
|
const dir: Record<string, number> = {};
|
||||||
for (const [id, reactions] of Object.entries(eventReactions)) {
|
for (const [id, reactions] of Object.entries(eventReactions)) {
|
||||||
const grouped = groupReactions(reactions);
|
const grouped = groupReactions(reactions);
|
||||||
const { vote } = getCommunityPostVote(grouped);
|
const { vote } = getEventReactionScore(grouped);
|
||||||
dir[id] = vote;
|
dir[id] = vote;
|
||||||
}
|
}
|
||||||
return dir;
|
return dir;
|
||||||
|
@@ -6,13 +6,13 @@ import useReplaceableEvent from "../../hooks/use-replaceable-event";
|
|||||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||||
import { WIKI_PAGE_KIND, getPageTitle } from "../../helpers/nostr/wiki";
|
import { WIKI_PAGE_KIND, getPageTitle } from "../../helpers/nostr/wiki";
|
||||||
import Timestamp from "../../components/timestamp";
|
import Timestamp from "../../components/timestamp";
|
||||||
import DebugEventButton from "../../components/debug-modal/debug-event-button";
|
|
||||||
import WikiPageHeader from "./components/wiki-page-header";
|
import WikiPageHeader from "./components/wiki-page-header";
|
||||||
import DiffViewer from "../../components/diff/diff-viewer";
|
import DiffViewer from "../../components/diff/diff-viewer";
|
||||||
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
|
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
|
||||||
import MarkdownContent from "./components/markdown";
|
import MarkdownContent from "./components/markdown";
|
||||||
import { WIKI_RELAYS } from "../../const";
|
import { WIKI_RELAYS } from "../../const";
|
||||||
import UserName from "../../components/user/user-name";
|
import UserName from "../../components/user/user-name";
|
||||||
|
import WikiPageMenu from "./components/wioki-page-menu";
|
||||||
|
|
||||||
function WikiComparePage({ base, diff }: { base: NostrEvent; diff: NostrEvent }) {
|
function WikiComparePage({ base, diff }: { base: NostrEvent; diff: NostrEvent }) {
|
||||||
const vertical = useBreakpointValue({ base: true, lg: false }) ?? false;
|
const vertical = useBreakpointValue({ base: true, lg: false }) ?? false;
|
||||||
@@ -24,8 +24,8 @@ function WikiComparePage({ base, diff }: { base: NostrEvent; diff: NostrEvent })
|
|||||||
|
|
||||||
<Flex gap="4" direction={vertical ? "column" : "row"}>
|
<Flex gap="4" direction={vertical ? "column" : "row"}>
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
<ButtonGroup float="right">
|
<ButtonGroup float="right" size="sm">
|
||||||
<DebugEventButton event={base} />
|
<WikiPageMenu page={base} aria-label="Page Optinos" />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<Heading>
|
<Heading>
|
||||||
<UserName pubkey={base.pubkey} />
|
<UserName pubkey={base.pubkey} />
|
||||||
@@ -35,8 +35,8 @@ function WikiComparePage({ base, diff }: { base: NostrEvent; diff: NostrEvent })
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flex={1}>
|
<Box flex={1}>
|
||||||
<ButtonGroup float="right">
|
<ButtonGroup float="right" size="sm">
|
||||||
<DebugEventButton event={diff} />
|
<WikiPageMenu page={diff} aria-label="Page Optinos" />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
<Heading>
|
<Heading>
|
||||||
<UserName pubkey={diff.pubkey} />
|
<UserName pubkey={diff.pubkey} />
|
||||||
|
@@ -6,6 +6,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Divider,
|
Divider,
|
||||||
|
Flex,
|
||||||
Heading,
|
Heading,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Spinner,
|
Spinner,
|
||||||
@@ -34,6 +35,7 @@ import NoteZapButton from "../../components/note/note-zap-button";
|
|||||||
import ZapBubbles from "../../components/note/timeline-note/components/zap-bubbles";
|
import ZapBubbles from "../../components/note/timeline-note/components/zap-bubbles";
|
||||||
import QuoteRepostButton from "../../components/note/quote-repost-button";
|
import QuoteRepostButton from "../../components/note/quote-repost-button";
|
||||||
import WikiPageMenu from "./components/wioki-page-menu";
|
import WikiPageMenu from "./components/wioki-page-menu";
|
||||||
|
import EventVoteButtons from "../../components/reactions/event-vote-buttions";
|
||||||
|
|
||||||
function ForkAlert({ page, address }: { page: NostrEvent; address: nip19.AddressPointer }) {
|
function ForkAlert({ page, address }: { page: NostrEvent; address: nip19.AddressPointer }) {
|
||||||
const topic = getPageTopic(page);
|
const topic = getPageTopic(page);
|
||||||
@@ -103,22 +105,27 @@ function WikiPagePage({ page }: { page: NostrEvent }) {
|
|||||||
<VerticalPageLayout>
|
<VerticalPageLayout>
|
||||||
<WikiPageHeader />
|
<WikiPageHeader />
|
||||||
|
|
||||||
<Box>
|
<Flex wrap="wrap">
|
||||||
<ButtonGroup float="right">
|
<Box>
|
||||||
<QuoteRepostButton event={page} />
|
<Heading>{getPageTitle(page)}</Heading>
|
||||||
<NoteZapButton event={page} showEventPreview={false} />
|
<Text>
|
||||||
<WikiPageMenu page={page} aria-label="Page Options" />
|
by <UserLink pubkey={page.pubkey} /> - <Timestamp timestamp={page.created_at} />
|
||||||
</ButtonGroup>
|
</Text>
|
||||||
<Heading>{getPageTitle(page)}</Heading>
|
</Box>
|
||||||
<Text>
|
<Flex alignItems="flex-end" gap="2" ml="auto">
|
||||||
by <UserLink pubkey={page.pubkey} /> - <Timestamp timestamp={page.created_at} />
|
<EventVoteButtons event={page} inline chevrons={false} />
|
||||||
</Text>
|
<ButtonGroup size="sm">
|
||||||
{address && <ForkAlert page={page} address={address} />}
|
<QuoteRepostButton event={page} />
|
||||||
{defer?.address && <DeferAlert page={page} address={defer.address} />}
|
<NoteZapButton event={page} showEventPreview={false} />
|
||||||
<Divider my="2" />
|
<WikiPageMenu page={page} aria-label="Page Options" />
|
||||||
<MarkdownContent event={page} />
|
</ButtonGroup>
|
||||||
<ZapBubbles event={page} mt="4" />
|
</Flex>
|
||||||
</Box>
|
</Flex>
|
||||||
|
{address && <ForkAlert page={page} address={address} />}
|
||||||
|
{defer?.address && <DeferAlert page={page} address={defer.address} />}
|
||||||
|
<ZapBubbles event={page} />
|
||||||
|
<Divider />
|
||||||
|
<MarkdownContent event={page} />
|
||||||
|
|
||||||
{forks.length > 0 && (
|
{forks.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
Reference in New Issue
Block a user