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 { useMemo } from "react";
import { Button, ButtonGroup, ButtonGroupProps, ButtonProps, IconButton, Image, useDisclosure } from "@chakra-ui/react";
import { useCallback, useMemo } from "react";
import { NostrEvent } from "../types/nostr-event";
import useEventReactions from "../hooks/use-event-reactions";
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 { 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 }) {
const renderIcon = () => {
if (emoji === "+") return <LikeIcon w="0.8em" h="0.8em" />;
if (emoji === "-") return <DislikeIcon w="0.8em" h="0.8em" />;
if (url) return <Image src={url} w="0.8em" h="0.8em" title={emoji} alt={emoji} />;
return <span>{emoji}</span>;
};
if (count > 1) {
return (
<>
{renderIcon()}
<span>{count}</span>
</>
);
}
return renderIcon();
export function ReactionIcon({ emoji, url }: { emoji: string; url?: string }) {
if (emoji === "+") return <LikeIcon />;
if (emoji === "-") return <DislikeIcon />;
if (url) return <Image src={url} title={emoji} alt={emoji} />;
return <span>{emoji}</span>;
}
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 reactions = useEventReactions(event.id) ?? [];
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;
return (
<>
<Flex
maxW="lg"
overflow="hidden"
gap="1"
alignItems="center"
cursor="pointer"
onClick={detailsModal.onOpen}
{...props}
>
<ButtonGroup wrap="wrap" {...props}>
{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} />}
</>
);

View File

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

View File

@ -8,9 +8,11 @@ import {
CardFooter,
CardHeader,
CardProps,
Divider,
Flex,
IconButton,
Link,
useBreakpointValue,
} from "@chakra-ui/react";
import { NostrEvent } from "../../types/nostr-event";
import { UserAvatarLink } from "../user-avatar-link";
@ -51,6 +53,8 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => {
// 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 });
return (
<TrustProvider event={event}>
<ExpandProvider>
@ -70,29 +74,39 @@ export const Note = React.memo(({ event, variant = "outline" }: NoteProps) => {
<CardBody p="0">
<NoteContentWithWarning event={event} />
</CardBody>
<CardFooter padding="2" display="flex" gap="2">
<ButtonGroup size="sm" variant="link" isDisabled={account?.readonly ?? true}>
<RepostButton event={event} />
<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"
/>
<CardFooter padding="2" display="flex" gap="2" flexDirection="column" alignItems="flex-start">
{showReactions && showReactionsOnNewLine && (
<EventReactions event={event} variant="ghost" size="xs" spacing="1" />
)}
<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 gap="2" w="full" alignItems="center">
<ButtonGroup size="xs" variant="ghost" isDisabled={account?.readonly ?? true}>
<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>
</Card>
</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[] };
@ -17,3 +19,19 @@ export function groupReactions(reactions: NostrEvent[]) {
}
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 PeopleListProvider from "../../../providers/people-list-provider";
import PeopleListSelection from "../../../components/people-list-selection/people-list-selection";
import { RelayFavicon } from "../../../components/relay-favicon";
function RelayPage({ relay }: { relay: string }) {
const { info } = useRelayInfo(relay);
@ -30,8 +31,9 @@ function RelayPage({ relay }: { relay: string }) {
return (
<Flex direction="column" alignItems="stretch" gap="2" p="2">
<Flex gap="2" alignItems="center" wrap="wrap" justifyContent="space-between">
<Heading isTruncated size={{ base: "md", sm: "lg" }}>
<Flex gap="2" alignItems="center" wrap="wrap">
<RelayFavicon relay={relay} />
<Heading isTruncated size={{ base: "md", sm: "lg" }} mr="auto">
{relay}
{info?.payments_url && (
<Tag

View File

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