clean up reactions a little

This commit is contained in:
hzrd149
2023-08-28 19:31:12 -05:00
parent c79c292315
commit ffe14d9b47
6 changed files with 119 additions and 83 deletions

View File

@@ -1,52 +1,74 @@
import { Flex, FlexProps, Image, useDisclosure } from "@chakra-ui/react"; import { Button, ButtonGroup, ButtonGroupProps, ButtonProps, IconButton, Image, useDisclosure } from "@chakra-ui/react";
import { useMemo } from "react"; import { useCallback, useMemo } from "react";
import { NostrEvent } from "../types/nostr-event"; import { NostrEvent } from "../types/nostr-event";
import useEventReactions from "../hooks/use-event-reactions"; import useEventReactions from "../hooks/use-event-reactions";
import { DislikeIcon, LikeIcon } from "./icons"; import { DislikeIcon, LikeIcon } from "./icons";
import { groupReactions } from "../helpers/nostr/reactions"; import { draftEventReaction, groupReactions } from "../helpers/nostr/reactions";
import ReactionDetailsModal from "./reaction-details-modal"; import ReactionDetailsModal from "./reaction-details-modal";
import { useSigningContext } from "../providers/signing-provider";
import clientRelaysService from "../services/client-relays";
import NostrPublishAction from "../classes/nostr-publish-action";
import eventReactionsService from "../services/event-reactions";
export function ReactionIcon({ emoji, url, count }: { emoji: string; count: number; url?: string }) { export function ReactionIcon({ emoji, url }: { emoji: string; url?: string }) {
const renderIcon = () => { if (emoji === "+") return <LikeIcon />;
if (emoji === "+") return <LikeIcon w="0.8em" h="0.8em" />; if (emoji === "-") return <DislikeIcon />;
if (emoji === "-") return <DislikeIcon w="0.8em" h="0.8em" />; if (url) return <Image src={url} title={emoji} alt={emoji} />;
if (url) return <Image src={url} w="0.8em" h="0.8em" title={emoji} alt={emoji} />; return <span>{emoji}</span>;
return <span>{emoji}</span>;
};
if (count > 1) {
return (
<>
{renderIcon()}
<span>{count}</span>
</>
);
}
return renderIcon();
} }
export default function EventReactions({ event, ...props }: Omit<FlexProps, "children"> & { event: NostrEvent }) { function ReactionGroupButton({
emoji,
url,
count,
...props
}: Omit<ButtonProps, "leftIcon" | "children"> & { emoji: string; count: number; url?: string }) {
if (count <= 1) {
return <IconButton icon={<ReactionIcon emoji={emoji} url={url} />} aria-label="Reaction" {...props} />;
}
return (
<Button leftIcon={<ReactionIcon emoji={emoji} url={url} />} {...props}>
{count > 1 && count}
</Button>
);
}
export default function EventReactions({
event,
...props
}: Omit<ButtonGroupProps, "children"> & { event: NostrEvent }) {
const detailsModal = useDisclosure(); const detailsModal = useDisclosure();
const reactions = useEventReactions(event.id) ?? []; const reactions = useEventReactions(event.id) ?? [];
const grouped = useMemo(() => groupReactions(reactions), [reactions]); const grouped = useMemo(() => groupReactions(reactions), [reactions]);
const { requestSignature } = useSigningContext();
const addReaction = useCallback(async (emoji = "+", url?: string) => {
const draft = draftEventReaction(event, emoji, url);
const signed = await requestSignature(draft);
if (signed) {
const writeRelays = clientRelaysService.getWriteUrls();
new NostrPublishAction("Reaction", writeRelays, signed);
eventReactionsService.handleEvent(signed);
}
}, []);
if (grouped.length === 0) return null; if (grouped.length === 0) return null;
return ( return (
<> <>
<Flex <ButtonGroup wrap="wrap" {...props}>
maxW="lg"
overflow="hidden"
gap="1"
alignItems="center"
cursor="pointer"
onClick={detailsModal.onOpen}
{...props}
>
{grouped.map((group) => ( {grouped.map((group) => (
<ReactionIcon key={group.emoji} emoji={group.emoji} url={group.url} count={group.count} /> <ReactionGroupButton
key={group.emoji}
emoji={group.emoji}
url={group.url}
count={group.count}
onClick={() => addReaction(group.emoji, group.url)}
/>
))} ))}
</Flex> <Button onClick={detailsModal.onOpen}>Show all</Button>
</ButtonGroup>
{detailsModal.isOpen && <ReactionDetailsModal isOpen onClose={detailsModal.onClose} reactions={reactions} />} {detailsModal.isOpen && <ReactionDetailsModal isOpen onClose={detailsModal.onClose} reactions={reactions} />}
</> </>
); );

View File

@@ -1,13 +1,6 @@
import { useMemo, useState } from "react";
import { import {
Box,
Button,
ButtonProps, ButtonProps,
Flex,
HStack,
IconButton, IconButton,
IconButtonProps,
Image,
Popover, Popover,
PopoverArrow, PopoverArrow,
PopoverBody, PopoverBody,
@@ -26,29 +19,16 @@ import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
import NostrPublishAction from "../../../classes/nostr-publish-action"; import NostrPublishAction from "../../../classes/nostr-publish-action";
import { AddReactionIcon } from "../../icons"; import { AddReactionIcon } from "../../icons";
import ReactionPicker from "../../reaction-picker"; import ReactionPicker from "../../reaction-picker";
import { draftEventReaction } from "../../../helpers/nostr/reactions";
export default function ReactionButton({ export default function ReactionButton({ event, ...props }: { event: NostrEvent } & Omit<ButtonProps, "children">) {
event: note,
...props
}: { event: NostrEvent } & Omit<ButtonProps, "children">) {
const { requestSignature } = useSigningContext(); const { requestSignature } = useSigningContext();
const account = useCurrentAccount(); const reactions = useEventReactions(event.id) ?? [];
const reactions = useEventReactions(note.id) ?? [];
const addReaction = async (emoji = "+", url?: string) => { const addReaction = async (emoji = "+", url?: string) => {
const event: DraftNostrEvent = { const draft = draftEventReaction(event, emoji, url);
kind: Kind.Reaction,
content: url ? ":" + emoji + ":" : emoji,
tags: [
["e", note.id],
["p", note.pubkey], // TODO: pick a relay for the user
],
created_at: dayjs().unix(),
};
if (url) event.tags.push(["emoji", emoji, url]); const signed = await requestSignature(draft);
const signed = await requestSignature(event);
if (signed) { if (signed) {
const writeRelays = clientRelaysService.getWriteUrls(); const writeRelays = clientRelaysService.getWriteUrls();
new NostrPublishAction("Reaction", writeRelays, signed); new NostrPublishAction("Reaction", writeRelays, signed);

View File

@@ -8,9 +8,11 @@ import {
CardFooter, CardFooter,
CardHeader, CardHeader,
CardProps, CardProps,
Divider,
Flex, Flex,
IconButton, IconButton,
Link, Link,
useBreakpointValue,
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { NostrEvent } from "../../types/nostr-event"; import { NostrEvent } from "../../types/nostr-event";
import { UserAvatarLink } from "../user-avatar-link"; import { UserAvatarLink } from "../user-avatar-link";
@@ -51,6 +53,8 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => {
// find mostr external link // find mostr external link
const externalLink = useMemo(() => event.tags.find((t) => t[0] === "mostr" || t[0] === "proxy"), [event])?.[1]; const externalLink = useMemo(() => event.tags.find((t) => t[0] === "mostr" || t[0] === "proxy"), [event])?.[1];
const showReactionsOnNewLine = useBreakpointValue({ base: true, md: false });
return ( return (
<TrustProvider event={event}> <TrustProvider event={event}>
<ExpandProvider> <ExpandProvider>
@@ -70,29 +74,39 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => {
<CardBody p="0"> <CardBody p="0">
<NoteContentWithWarning event={event} /> <NoteContentWithWarning event={event} />
</CardBody> </CardBody>
<CardFooter padding="2" display="flex" gap="2"> <CardFooter padding="2" display="flex" gap="2" flexDirection="column" alignItems="flex-start">
<ButtonGroup size="sm" variant="link" isDisabled={account?.readonly ?? true}> {showReactions && showReactionsOnNewLine && (
<RepostButton event={event} /> <EventReactions event={event} variant="ghost" size="xs" spacing="1" />
<QuoteRepostButton event={event} />
<NoteZapButton event={event} size="sm" />
{showReactions && <EventReactions event={event} />}
<ReactionButton event={event} size="sm" />
</ButtonGroup>
<Box flexGrow={1} />
{externalLink && (
<IconButton
as={Link}
icon={<ExternalLinkIcon />}
aria-label="Open External"
href={externalLink}
size="sm"
variant="link"
target="_blank"
/>
)} )}
<EventRelays event={event} /> <Flex gap="2" w="full" alignItems="center">
<BookmarkButton event={event} aria-label="Bookmark note" size="sm" variant="link" /> <ButtonGroup size="xs" variant="ghost" isDisabled={account?.readonly ?? true}>
<NoteMenu event={event} size="sm" variant="link" aria-label="More Options" /> <RepostButton event={event} />
<QuoteRepostButton event={event} />
<NoteZapButton event={event} />
<ReactionButton event={event} />
</ButtonGroup>
{showReactions && !showReactionsOnNewLine && (
<>
<Divider orientation="vertical" h="1.5rem" />
<EventReactions event={event} variant="ghost" size="xs" spacing="1" />
</>
)}
<Box flexGrow={1} />
{externalLink && (
<IconButton
as={Link}
icon={<ExternalLinkIcon />}
aria-label="Open External"
href={externalLink}
size="sm"
variant="link"
target="_blank"
/>
)}
<EventRelays event={event} />
<BookmarkButton event={event} aria-label="Bookmark note" size="sm" variant="link" />
<NoteMenu event={event} size="sm" variant="link" aria-label="More Options" />
</Flex>
</CardFooter> </CardFooter>
</Card> </Card>
</ExpandProvider> </ExpandProvider>

View File

@@ -1,4 +1,6 @@
import { NostrEvent } from "../../types/nostr-event"; import { Kind } from "nostr-tools";
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
import dayjs from "dayjs";
export type ReactionGroup = { emoji: string; url?: string; name?: string; count: number; pubkeys: string[] }; export type ReactionGroup = { emoji: string; url?: string; name?: string; count: number; pubkeys: string[] };
@@ -17,3 +19,19 @@ export function groupReactions(reactions: NostrEvent[]) {
} }
return Array.from(Object.values(groups)).sort((a, b) => b.count - a.count); return Array.from(Object.values(groups)).sort((a, b) => b.count - a.count);
} }
export function draftEventReaction(event: NostrEvent, emoji = "+", url?: string) {
const draft: DraftNostrEvent = {
kind: Kind.Reaction,
content: url ? ":" + emoji + ":" : emoji,
tags: [
["e", event.id],
["p", event.pubkey], // TODO: pick a relay for the user
],
created_at: dayjs().unix(),
};
if (url) draft.tags.push(["emoji", emoji, url]);
return draft;
}

View File

@@ -23,6 +23,7 @@ import RelayReviews from "./relay-reviews";
import RelayNotes from "./relay-notes"; import RelayNotes from "./relay-notes";
import PeopleListProvider from "../../../providers/people-list-provider"; import PeopleListProvider from "../../../providers/people-list-provider";
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection"; import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
import { RelayFavicon } from "../../../components/relay-favicon";
function RelayPage({ relay }: { relay: string }) { function RelayPage({ relay }: { relay: string }) {
const { info } = useRelayInfo(relay); const { info } = useRelayInfo(relay);
@@ -30,8 +31,9 @@ function RelayPage({ relay }: { relay: string }) {
return ( return (
<Flex direction="column" alignItems="stretch" gap="2" p="2"> <Flex direction="column" alignItems="stretch" gap="2" p="2">
<Flex gap="2" alignItems="center" wrap="wrap" justifyContent="space-between"> <Flex gap="2" alignItems="center" wrap="wrap">
<Heading isTruncated size={{ base: "md", sm: "lg" }}> <RelayFavicon relay={relay} />
<Heading isTruncated size={{ base: "md", sm: "lg" }} mr="auto">
{relay} {relay}
{info?.payments_url && ( {info?.payments_url && (
<Tag <Tag

View File

@@ -25,7 +25,7 @@ export default function RelayNotes({ relay }: { relay: string }) {
const timelineEventFilter = useTimelinePageEventFilter(); const timelineEventFilter = useTimelinePageEventFilter();
const eventFilter = useCallback( const eventFilter = useCallback(
(event: NostrEvent) => { (event: NostrEvent) => {
if (!isReply(event)) return false; if (isReply(event)) return false;
return timelineEventFilter(event); return timelineEventFilter(event);
}, },
[timelineEventFilter], [timelineEventFilter],