add vote buttons to wiki pages

This commit is contained in:
hzrd149 2024-04-20 09:39:29 -05:00
parent 260e2433b1
commit 3e2906e66b
9 changed files with 123 additions and 100 deletions

View File

@ -40,9 +40,7 @@ export default function EmbeddedWikiPage({ page: page, ...props }: Omit<CardProp
</Text>
</CardHeader>
<CardBody p="2" overflow="hidden">
<Text color="GrayText" noOfLines={2}>
{getPageSummary(page)}
</Text>
<Text noOfLines={2}>{getPageSummary(page)}</Text>
</CardBody>
{showFooter && (
<CardFooter>

View 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>
);
}

View File

@ -85,13 +85,6 @@ export function buildApprovalMap(events: Iterable<NostrEvent>, mods: string[]) {
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) {
const communityTag = event.tags.filter(isATag).find((t) => t[1].startsWith(COMMUNITY_DEFINITION_KIND + ":"));
return communityTag ? parseCoordinate(communityTag[1], true) : null;

View File

@ -41,3 +41,10 @@ export function draftEventReaction(event: NostrEvent, emoji = "+", url?: string)
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 };
}

View File

@ -1,7 +1,7 @@
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 { NostrEvent } from "../../../types/nostr-event";
@ -9,7 +9,9 @@ const ApprovedEvent = memo(
({ event, approvals, showCommunity }: { event: NostrEvent; approvals: NostrEvent[]; showCommunity?: boolean }) => {
return (
<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} />
</Flex>
);

View File

@ -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>
);
}

View File

@ -5,7 +5,6 @@ import {
COMMUNITY_APPROVAL_KIND,
buildApprovalMap,
getCommunityMods,
getCommunityPostVote,
getCommunityRelays,
} from "../../../helpers/nostr/communities";
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 useUserMuteFilter from "../../../hooks/use-user-mute-filter";
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 { RouterContext } from "../community-home";
@ -40,7 +39,7 @@ export default function CommunityTrendingView() {
const dir: Record<string, number> = {};
for (const [id, reactions] of Object.entries(eventReactions)) {
const grouped = groupReactions(reactions);
const { vote } = getCommunityPostVote(grouped);
const { vote } = getEventReactionScore(grouped);
dir[id] = vote;
}
return dir;

View File

@ -6,13 +6,13 @@ import useReplaceableEvent from "../../hooks/use-replaceable-event";
import VerticalPageLayout from "../../components/vertical-page-layout";
import { WIKI_PAGE_KIND, getPageTitle } from "../../helpers/nostr/wiki";
import Timestamp from "../../components/timestamp";
import DebugEventButton from "../../components/debug-modal/debug-event-button";
import WikiPageHeader from "./components/wiki-page-header";
import DiffViewer from "../../components/diff/diff-viewer";
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
import MarkdownContent from "./components/markdown";
import { WIKI_RELAYS } from "../../const";
import UserName from "../../components/user/user-name";
import WikiPageMenu from "./components/wioki-page-menu";
function WikiComparePage({ base, diff }: { base: NostrEvent; diff: NostrEvent }) {
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"}>
<Box flex={1}>
<ButtonGroup float="right">
<DebugEventButton event={base} />
<ButtonGroup float="right" size="sm">
<WikiPageMenu page={base} aria-label="Page Optinos" />
</ButtonGroup>
<Heading>
<UserName pubkey={base.pubkey} />
@ -35,8 +35,8 @@ function WikiComparePage({ base, diff }: { base: NostrEvent; diff: NostrEvent })
</Text>
</Box>
<Box flex={1}>
<ButtonGroup float="right">
<DebugEventButton event={diff} />
<ButtonGroup float="right" size="sm">
<WikiPageMenu page={diff} aria-label="Page Optinos" />
</ButtonGroup>
<Heading>
<UserName pubkey={diff.pubkey} />

View File

@ -6,6 +6,7 @@ import {
Button,
ButtonGroup,
Divider,
Flex,
Heading,
SimpleGrid,
Spinner,
@ -34,6 +35,7 @@ import NoteZapButton from "../../components/note/note-zap-button";
import ZapBubbles from "../../components/note/timeline-note/components/zap-bubbles";
import QuoteRepostButton from "../../components/note/quote-repost-button";
import WikiPageMenu from "./components/wioki-page-menu";
import EventVoteButtons from "../../components/reactions/event-vote-buttions";
function ForkAlert({ page, address }: { page: NostrEvent; address: nip19.AddressPointer }) {
const topic = getPageTopic(page);
@ -103,22 +105,27 @@ function WikiPagePage({ page }: { page: NostrEvent }) {
<VerticalPageLayout>
<WikiPageHeader />
<Box>
<ButtonGroup float="right">
<QuoteRepostButton event={page} />
<NoteZapButton event={page} showEventPreview={false} />
<WikiPageMenu page={page} aria-label="Page Options" />
</ButtonGroup>
<Heading>{getPageTitle(page)}</Heading>
<Text>
by <UserLink pubkey={page.pubkey} /> - <Timestamp timestamp={page.created_at} />
</Text>
{address && <ForkAlert page={page} address={address} />}
{defer?.address && <DeferAlert page={page} address={defer.address} />}
<Divider my="2" />
<MarkdownContent event={page} />
<ZapBubbles event={page} mt="4" />
</Box>
<Flex wrap="wrap">
<Box>
<Heading>{getPageTitle(page)}</Heading>
<Text>
by <UserLink pubkey={page.pubkey} /> - <Timestamp timestamp={page.created_at} />
</Text>
</Box>
<Flex alignItems="flex-end" gap="2" ml="auto">
<EventVoteButtons event={page} inline chevrons={false} />
<ButtonGroup size="sm">
<QuoteRepostButton event={page} />
<NoteZapButton event={page} showEventPreview={false} />
<WikiPageMenu page={page} aria-label="Page Options" />
</ButtonGroup>
</Flex>
</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 && (
<>